Saltar al contenido principal
Capítulo 3.1 intermedio ses suavizado exponencial alpha statsmodels

Suavizado exponencial simple, paso a paso

6 min lectura
Por qué los pesos del suavizado exponencial decaen geométricamente, cómo optimizar alpha y en qué series SES es realmente la herramienta correcta.

En el capítulo principal viste a SES fallar: aplicado a una serie con tendencia, el parámetro α se disparaba a 1 y el modelo se convertía en un naive con nombre elegante. Eso no significa que SES sea un modelo débil — significa que lo aplicamos donde no encaja. Aquí lo vas a ver funcionar de verdad, y vas a entender el mecanismo con suficiente detalle para saber exactamente cuándo usarlo.


De la fórmula recursiva a la suma ponderada

La fórmula recursiva que ya conoces:

y^t+1=αyt+(1α)y^t\hat{y}_{t+1} = \alpha \, y_t + (1 - \alpha) \, \hat{y}_t

parece decir poco sobre “memoria”. Pero si sustituyes y^t\hat{y}_t por su propia definición, y repites el proceso hacia atrás, llegas a esto:

y^t+1=αk=0t(1α)kytk\hat{y}_{t+1} = \alpha \sum_{k=0}^{t} (1-\alpha)^{k} \, y_{t-k}

Cada observación pasada entra en el pronóstico con un peso α(1α)k\alpha(1-\alpha)^k, donde kk es cuántos períodos atrás está. Como (1α)<1(1-\alpha) < 1, esos pesos se hacen cada vez más pequeños cuanto más lejos miras — de ahí el nombre “exponencial”.

import numpy as np
import matplotlib.pyplot as plt

def pesos_ses(alpha: float, n_periodos: int) -> np.ndarray:
    """Calcula los pesos que SES asigna a las últimas n_periodos observaciones."""
    k = np.arange(n_periodos)
    return alpha * (1 - alpha) ** k

# Comparamos cómo cambia la forma de los pesos con distintos alpha
fig, ax = plt.subplots(figsize=(10, 4))
for alpha, color in zip([0.1, 0.3, 0.7], ["#1a1a1a", "#ffcc00", "#ff8b94"]):
    pesos = pesos_ses(alpha, 15)
    ax.plot(pesos, marker="o", markersize=4, color=color, label=f"α = {alpha}")

ax.set_title("Peso asignado a cada observación pasada según alpha")
ax.set_xlabel("Períodos hacia atrás (k)")
ax.set_ylabel("Peso")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Una serie donde SES sí es la herramienta correcta

Trabajemos con una serie sin tendencia: el nivel se mueve muy despacio (una pequeña deriva aleatoria) y el ruido de observación es considerable. Es el escenario típico de SES: sin patrón de crecimiento claro, solo un nivel que hay que estimar con cuidado frente al ruido.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.holtwinters import SimpleExpSmoothing
from sklearn.metrics import mean_absolute_error

np.random.seed(7)
n = 40

# Nivel casi constante con una deriva muy suave, más ruido de observación
deriva = np.cumsum(np.random.normal(0, 2, n)) * 0.3
ruido  = np.random.normal(0, 12, n)
serie  = pd.Series(100 + deriva + ruido, name="serie_estable")

plt.figure(figsize=(10, 4))
plt.plot(serie, color="#1a1a1a", marker="o", markersize=3, linewidth=1)
plt.title("Serie sin tendencia clara — candidata natural para SES")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Ventana de media móvil sobre esta serie (para comparar)
Ventana: 5
Serie original Media móvil

Comparando varios alpha a mano

Antes de dejar que statsmodels optimice por ti, prueba varios valores manualmente. Es la mejor forma de sentir el trade-off entre reactividad y estabilidad.

train = serie.iloc[:-8]
test  = serie.iloc[-8:]

resultados = {}
for alpha in [0.1, 0.3, 0.5, 0.8]:
    modelo = SimpleExpSmoothing(train, initialization_method="estimated").fit(
        smoothing_level=alpha, optimized=False
    )
    pred = modelo.forecast(8)
    resultados[alpha] = mean_absolute_error(test, pred)

for alpha, mae in resultados.items():
    print(f"alpha = {alpha:.1f}  ->  MAE = {mae:.2f}")
alpha = 0.1  ->  MAE = 14.38
alpha = 0.3  ->  MAE = 14.45
alpha = 0.5  ->  MAE = 15.20
alpha = 0.8  ->  MAE = 16.91

Cuanto más bajo el α, mejor el resultado. Tiene sentido: la serie apenas tiene deriva real, así que suavizar mucho (dar poco peso a cada observación individual) filtra el ruido sin perder información real sobre el nivel.

# Dejamos que statsmodels optimice alpha por mínimos cuadrados
modelo_opt = SimpleExpSmoothing(train, initialization_method="estimated").fit()
print(f"Alpha óptimo: {modelo_opt.params['smoothing_level']:.3f}")

pred_opt = modelo_opt.forecast(8)
mae_opt = mean_absolute_error(test, pred_opt)
mae_naive = mean_absolute_error(test, pd.Series(train.iloc[-1], index=test.index))

print(f"MAE SES (alpha óptimo): {mae_opt:.2f}")
print(f"MAE naive             : {mae_naive:.2f}")

El optimizador converge a un α muy bajo (cercano a 0), confirmando lo que ya viste a mano: aquí conviene suavizar fuerte. El MAE de SES (~14.6) mejora claramente al naive (~19.0) — casi un 23% menos de error. Esta es la cara buena de SES que el capítulo principal no te enseñó.


Ponte a prueba

Genera esta serie y decide, antes de ejecutar nada, si esperas que SES funcione bien o degenere como en el capítulo principal.

import numpy as np
import pandas as pd

np.random.seed(33)
n = 50
t = np.arange(n)

# ¿Tiene tendencia? Mira los coeficientes antes de decidir
serie_reto = pd.Series(
    300 + 0.05 * t + np.random.normal(0, 18, n)   # pendiente casi nula
)
Ver solución

La pendiente (0.05 * t) es casi nula frente al ruido (sigma = 18): a lo largo de 50 períodos, la tendencia solo desplaza el nivel unos 2.5 puntos, un valor irrisorio comparado con la dispersión del ruido. Es, en la práctica, una serie sin tendencia relevante.

from statsmodels.tsa.holtwinters import SimpleExpSmoothing
from sklearn.metrics import mean_absolute_error

train_r, test_r = serie_reto.iloc[:-10], serie_reto.iloc[-10:]

modelo_r = SimpleExpSmoothing(train_r, initialization_method="estimated").fit()
print(f"Alpha: {modelo_r.params['smoothing_level']:.3f}")

pred_r = modelo_r.forecast(10)
print(f"MAE SES: {mean_absolute_error(test_r, pred_r):.2f}")

Deberías obtener un α moderado-bajo (no degenerado en 0 ni en 1) y un MAE competitivo frente al naive. La lección: no es la presencia matemática de una pendiente lo que rompe SES, es que esa pendiente sea grande en relación al ruido.


Para llevarte

  • El peso que SES asigna a cada observación pasada decae geométricamente: α(1α)k\alpha(1-\alpha)^k. Por eso un α alto “olvida” rápido y uno bajo “recuerda” mucho.
  • SES es la herramienta correcta cuando la serie es aproximadamente estacionaria — mismo test ADF que usaste para ARIMA te lo confirma antes de ajustar nada.
  • Un α que el optimizador empuja a 0 o a 1 no es un error: es información sobre si el modelo encaja con la estructura de tus datos.
  • Cuando hay tendencia real (grande frente al ruido), SES no es suficiente — el siguiente subcapítulo introduce el método de Holt para resolver justo eso.