Por qué tu pronóstico falla en Navidad: Manejo de múltiples estacionalidades y picos navideños
La última vez, Nora recurrió a Prophet para el quiebre estructural cuando la cadena de panaderías abrió una nueva tienda. Su detección automática de puntos de cambio ajustó la tendencia a la nueva realidad de ventas; ARIMA seguía arrastrando los pronósticos hacia la antigua línea base.
El problema del ‘helado a medianoche’
Este es el problema insignia de Nora. Cada diciembre, el pronóstico de su cadena de panaderías se desmorona cuando la demanda de pasteles navideños choca con los ritmos normales diarios, semanales y anuales de la tienda. Digamos que administras una cafetería. Si te pidiera predecir cuántos lattes venderás el próximo martes a las 8:00 AM, no solo mirarías las ventas de ayer. Instintivamente revisarías tres “latidos” en tus datos. El ciclo diario: estás más ocupado a las 8:00 AM que a las 8:00 PM. El ciclo semanal: los martes suelen ser más lentos que los sábados. Y el ciclo anual: se venden más lattes calientes en diciembre que en julio.
Los datos no tienen un solo ritmo; tienen muchos latidos que se superponen. La mayoría de los modelos simples se confunden porque leen estas superposiciones como ruido aleatorio. Un modelo que solo rastrea la tendencia diaria no entenderá por qué un lunes por la mañana en diciembre es tres veces más ocupado que un lunes por la mañana en junio. El objetivo es enseñar al modelo a leer el calendario como lo hace un humano.
Así que construyamos un conjunto de datos que imite esta complejidad. Crearemos una señal con un ritmo diario, un aumento semanal y un pico festivo masivo.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from prophet import Prophet
# 1. Generate synthetic data: 2 years of hourly sales
dates = pd.date_range(start='2021-01-01', end='2022-12-31', freq='H')
n = len(dates)
# Daily heartbeat (busy at 8am and 5pm)
daily = 10 + 5 * np.sin(2 * np.pi * dates.hour / 24) + 5 * np.sin(4 * np.pi * dates.hour / 24)
# Weekly heartbeat (busier on weekends)
weekly = 5 * np.sin(2 * np.pi * dates.dayofweek / 7)
# Yearly heartbeat (busier in winter)
yearly = 10 * np.cos(2 * np.pi * dates.dayofyear / 365)
# The Christmas Spike (a massive jump around Dec 25)
holiday_spike = np.where((dates.month == 12) & (dates.day == 25), 50, 0)
# Combine everything with some random noise
y = daily + weekly + yearly + holiday_spike + np.random.normal(0, 2, n)
df = pd.DataFrame({'ds': dates, 'y': y})
plt.figure(figsize=(12, 6))
plt.plot(df['ds'], df['y'], alpha=0.5)
plt.title("The Overlapping Heartbeats of Sales Data")
plt.show()
from prophet import Prophet— importa Prophet del paqueteprophet(el nombrefbprophetse retiró hace años y ya no se instala en Python moderno); esta es la importación corregida.pd.date_range(start='2021-01-01', end='2022-12-31', freq='H')— genera un DatetimeIndex de marcas de tiempo por hora que abarcan exactamente dos años completos, dándole al modelo suficientes ciclos anuales para aprender el patrón de Navidad.5 * np.sin(2 * np.pi * dates.hour / 24)— una onda sinusoidal que completa un ciclo completo cada 24 horas; el factor2 * np.pi / 24mapea el reloj de 24 horas a una rotación, creando el pico matutino diario.5 * np.sin(4 * np.pi * dates.hour / 24)— el segundo armónico (frecuencia doble); completa dos ciclos por día, añadiendo el aumento de las 5 PM encima del pico matutino.5 * np.sin(2 * np.pi * dates.dayofweek / 7)— una onda semanal donde 7 días se mapean a una rotación; esto hace que los fines de semana sean sistemáticamente diferentes de los días laborables.10 * np.cos(2 * np.pi * dates.dayofyear / 365)— una onda anual usando el coseno (desplazada 90° del seno) para que el pico caiga en invierno (diciembre) en lugar de en primavera.np.where((dates.month == 12) & (dates.day == 25), 50, 0)— una máscara booleana que añade +50 solo el 25 de diciembre de cada año; el&combina las condiciones de mes y día elemento por elemento.np.random.normal(0, 2, n)— añade ruido gaussiano con media 0 y desviación estándar 2 a cada marca de tiempo, de modo que los patrones no sean perfectamente limpios y el modelo tenga que hacer un trabajo real.
Mira esta gráfica y verás una nube desordenada. Pero escondidos dentro hay patrones predecibles. Si no le decimos a nuestro modelo sobre estos ciclos específicos, los promediará, produciendo un pronóstico que es “aproximadamente correcto” pero específicamente incorrecto todas las mañanas y cada Navidad.
El calendario miente: por qué los días festivos son difíciles
Los días festivos no son solo días especialmente ocupados — son choques irregulares. Algunos se quedan en su lugar, como la Navidad el 25 de diciembre. Otros, como el Día del Trabajo o la Pascua, son objetivos en movimiento que cambian de un año a otro.
Lo que ocurre aquí es un fenómeno llamado Efecto Halo. La gente no solo compra en Black Friday. Comienzan a navegar el martes y siguen comprando hasta Cyber Monday. Si marcas solo el día festivo en sí, el modelo trata esos picos de ventas del miércoles y jueves como ruido inexplicado.
Así que tratamos un día festivo como un regresor de “evento especial”. Le decimos al modelo: esta ventana de tiempo sigue un conjunto de reglas diferente.
import holidays
# Create a holiday lookup table
us_holidays = holidays.US(years=[2021, 2022])
holiday_list = []
for date, name in sorted(us_holidays.items()):
holiday_list.append({
'holiday': name,
'ds': pd.to_datetime(date),
'lower_window': -2, # The 'Halo' starts 2 days before
'upper_window': 1 # And lasts 1 day after
})
holidays_df = pd.DataFrame(holiday_list)
print(holidays_df.head())
import holidays— importa el paqueteholidaysde Python, una biblioteca que proporciona listas prefabricadas de días festivos públicos para docenas de países.holidays.US(years=[2021, 2022])— crea un objeto similar a un diccionario que mapea objetos de fecha a los nombres de los días festivos federales de EE. UU. para los años especificados; llamar a.items()produce pares(date, name).sorted(us_holidays.items())— ordena las fechas de los días festivos cronológicamente para que las filas del DataFrame estén en orden calendario.'lower_window': -2— le indica a Prophet que el efecto del día festivo comienza 2 días antes de la fecha real del día festivo; este es el “halo” que captura las prisas de compras previas al día festivo.'upper_window': 1— extiende el efecto del día festivo 1 día después de la fecha oficial, capturando la demanda de cola posterior al día festivo.pd.DataFrame(holiday_list)— convierte la lista de diccionarios en un DataFrame con las columnasholiday,ds,lower_windowyupper_window— el esquema exacto que Prophet espera.
Añadir un lower_window le indica al modelo que el “efecto Navidad” comienza el 23 de diciembre. De esta manera, no es tomado por sorpresa por la prisa previa al día festivo.
Días festivos como indicadores de fecha simples frente a regresor de ventana halo — ¿cuándo vale la pena la complejidad adicional?
| Enfoque | Cómo funciona | Cuándo usarlo | Cuándo es excesivo |
|---|---|---|---|
| Indicador de fecha simple | Marca solo el día del día festivo en sí (p. ej., 25 dic.) como una columna binaria 1/0 — el modelo ve un aumento de un solo día | El efecto del día festivo es verdaderamente un pico de un día sin acumulación previa ni cola significativas (p. ej., un festival o promoción local de un día) | Cuando los clientes comienzan a comprar días o semanas antes del evento, o cuando las ventas de liquidación posteriores al evento extienden el efecto |
Regresor de ventana halo (lower_window / upper_window) | Marca el día festivo más una ventana de días antes y después; Prophet aplica el efecto del día festivo en toda la ventana | El día festivo tiene una acumulación de varios días (compras de regalos de Navidad, preparación de la comida de Acción de Gracias) o una cola posterior al evento (Cyber Monday después de Black Friday) | Cuando el día festivo es genuinamente un evento de un día y la ventana adicional solo agrega ruido — el modelo puede atribuir variación aleatoria al “halo” y alucinar una acumulación previa que no existe |
La decisión clave: la ventana halo vale la complejidad adicional cuando puedes ver en tus datos que las ventas comienzan a subir días antes de la fecha oficial del día festivo. Si el pico es verdaderamente de un día y plano el resto de la semana, un indicador de fecha simple es más limpio y menos propenso al sobreajuste. Para la panadería de Nora, donde los pedidos anticipados de pastelería navideña comienzan a llegar para el 20 de diciembre, la ventana halo es esencial — sin ella, el modelo trata la prisa previa a la Navidad como ruido y subestima la semana más ocupada del año.
Descomposición de la señal (La ‘parte más difícil’)
Aquí está la parte más difícil del pronóstico causal: Series de Fourier. Piensa en ello como sintonizar una radio. Una estación transmite en una frecuencia específica. Los términos de Fourier son ondas suaves que el modelo usa para aproximar los baches en tus datos.
Supongamos que tienes un ciclo de ventas complejo de 4 días—quizás organizas un festival de música que ocurre cada 4 días. No puedes simplemente usar “Día de la semana”. Necesitas decirle al modelo que busque una onda que se repite cada 96 horas.
Luego viene la Escala previa de estacionalidad. Controla cuánto se mueve el modelo. Configúrala alta y el modelo persigue cada pequeño bache en los datos. Configúrala baja y se mantiene suave para ignorar los valores atípicos.
Prophet modela cada componente estacional como una serie de Fourier — una suma de ondas seno y coseno en armónicos del período base:
| Español simple | Símbolo estadístico | Equivalente en Python |
|---|---|---|
| Componente estacional (suma de todos los pares de ondas) | forecast['weekly'], forecast['yearly'], forecast['daily'] | |
| Período de un ciclo completo (p. ej., 7 días, 365.25 días) | period=4, period=7, period=365.25 | |
| Orden de Fourier — número de pares seno–coseno usados | fourier_order=10, fourier_order=3 | |
| Coeficiente coseno para el -ésimo armónico | Ajustado internamente por Prophet (no expuesto directamente) | |
| Coeficiente seno para el -ésimo armónico | Ajustado internamente por Prophet (no expuesto directamente) |
Un fourier_order () más alto significa que se apilan más pares de ondas, lo que permite que la curva estacional se flexione en formas más pronunciadas e irregulares — pero también aumenta el riesgo de sobreajuste al ruido. Un más bajo fuerza una onda más suave y simple que puede pasar por alto los picos pronunciados.
# Initialize Prophet with our holiday list
model = Prophet(holidays=holidays_df,
yearly_seasonality=10,
weekly_seasonality=True,
daily_seasonality=True)
# Add a custom seasonality for a specific business cycle (e.g., every 4 days)
model.add_seasonality(name='four_day_cycle', period=4, fourier_order=3)
model.fit(df)
forecast = model.predict(df)
Prophet(holidays=holidays_df, ...)— crea un modelo Prophet que aplicará los efectos de días festivos deholidays_dfademás de sus componentes estacionales integrados.yearly_seasonality=10— establece el orden de Fourier del componente anual en 10 (también el valor por defecto); esto significa que se usan 10 pares seno–coseno para aproximar la forma del ciclo anual, permitiendo capturar patrones complejos dentro del año como el aumento de Navidad.weekly_seasonality=True/daily_seasonality=True— habilita las estacionalidades semanales (orden de Fourier 3) y diarias (orden de Fourier 4) integradas de Prophet con sus configuraciones por defecto.model.add_seasonality(name='four_day_cycle', period=4, fourier_order=3)— registra un componente estacional personalizado que se repite cada 4 unidades de tiempo (horas, en este conjunto de datos), usando 3 pares de Fourier para dar forma a la onda; así es como le enseñas a Prophet sobre ciclos no estándar que no se alinean con semanas de 7 días o años de 365 días.model.fit(df)— ajusta todos los componentes (tendencia, días festivos, diario, semanal, anual y el ciclo personalizado de 4 días) simultáneamente en el DataFrame completo.model.predict(df)— genera predicciones dentro de la muestra para el mismo DataFrame; cada fila en la salida obtiene desgloses a nivel de componente (yhat,trend,weekly,yearly,holidays, etc.).
En este código, fourier_order=3 establece la complejidad de la onda. Un número más alto permite patrones más irregulares. Mantenlo bajo para una curva de ventas suave. Súbelo si tus datos tienen picos y valles pronunciados.
Leyendo las hojas de té: Interpretación de los componentes
Una vez que el modelo se ajusta, necesitamos comprobar si realmente aprendió algo. Prophet descompone el pronóstico en sus componentes.
fig = model.plot_components(forecast)
plt.show()
model.plot_components(forecast)— genera una figura con un subgráfico por componente (tendencia, semanal, diario, anual, días festivos y cualquier estacionalidad personalizada comofour_day_cycle); cada subgráfico muestra cuánto añade o resta ese componente a la línea base en cada punto en el tiempo.plt.show()— renderiza la figura en el notebook o la pantalla actual.
Estos gráficos muestran lo que tu modelo cree que está impulsando tus ventas:
- Gráfico semanal: Una línea que alcanza un máximo de 5.0 el sábado significa que el sábado añade 5 unidades a tu línea base.
- Gráfico de días festivos: Un pico de 50 unidades el 25 de diciembre significa que el modelo aisló el efecto de la Navidad de la tendencia de invierno.
- Gráfico diario: La hora pico de las 8:00 AM se muestra claramente.
Aquí está la prueba real. Si tu componente de días festivos muestra un pico de 50 unidades y las ventas reales aumentaron en 52, el modelo está funcionando bien. Pero si el componente muestra solo 10 mientras las ventas aumentaron en 50, presenta subajuste; es demasiado conservador respecto al impacto de los días festivos.
Cuando el modelo alucina: Errores comunes
Aquí está el problema: los modelos pueden ser un poco demasiado astutos. Esta es la trampa del Super Bowl. Si solo tienes un año de datos y le dices al modelo que el domingo del Super Bowl es un día festivo, ve un pico y decide que ese domingo específico siempre es especial. No sabe por qué.
Con muy pocos datos, un seasonality_prior_scale alto hace que el modelo “alucine” patrones que no existen. Podría decidir que cada martes es especial porque un martes del mes pasado estuvo extrañamente ocupado.
# Example of Overfitting: Setting prior scale too high with limited data
overfit_model = Prophet(holidays=holidays_df,
holidays_prior_scale=20.0) # Dangerously high!
overfit_model.fit(df.iloc[:365*24]) # Only give it one year
holidays_prior_scale=20.0— establece la escala previa para los efectos de días festivos en 20 (el valor predeterminado es 10); un valor más alto le indica a Prophet que confíe más en los datos y aplique efectos festivos más grandes, lo que corre el riesgo de que el modelo persiga el ruido como si fuera un pico festivo real.df.iloc[:365*24]— recorta el DataFrame para incluir solo las primeras 8,760 filas (365 días × 24 horas), dándole al modelo únicamente un año de datos; con solo una Navidad en el conjunto de entrenamiento, el modelo no puede distinguir un patrón anual recurrente de un suceso aislado.
Así que tu modelo es tan inteligente como el historial que le proporciones. Si quieres pronosticar la Navidad de 2023, necesitas mostrarle las Navidades de 2021 y 2022. Sin dos puntos de referencia, el modelo no puede distinguir entre una tendencia anual y un suceso aislado.
Lista de verificación de resumen
- Identifica los ritmos: ¿Tus datos se mueven por hora, día o mes? Usa
add_seasonalitypara cualquier cosa que no sea estándar. - Usa ventanas: No solo marques el feriado; marca los días previos con
lower_window. - Revisa los componentes: Ejecuta
plot_componentsy confirma que la historia del modelo coincida con tu intuición de negocio. - Cuida los priors: Si el modelo reacciona exageradamente a eventos puntuales, disminuye
holidays_prior_scale.
Ahora que las peculiaridades del calendario están resueltas, tus pronósticos deberían sostenerse durante la temporada alta de los feriados. Prueba variar fourier_order y observa cómo cambia la flexibilidad de tu curva. El pronóstico de Nora por fin maneja la Navidad correctamente. Pero acaba de darse cuenta de que su compañero de equipo ha estado validando estos mismos modelos de panadería con validación cruzada K-Fold simple, lo que permite silenciosamente que los datos futuros se filtren en el conjunto de entrenamiento.
Comprueba tu comprensión
Las preguntas a continuación avanzan desde el simple recuerdo hasta el diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.
Recordar ¿Cuáles son los tres “latidos” superpuestos que el artículo indica que contienen los datos de ventas de una cafetería y en qué escala de tiempo opera cada uno?
Comprender
Explica con tus propias palabras qué hacen lower_window=-2 y upper_window=1 en el dataframe de días festivos, y por qué el Efecto Halo hace que esas ventanas sean necesarias.
Aplicar El componente de días festivos de tu modelo muestra un pico de 10 para la Navidad, pero las ventas reales aumentaron en 50. Usando la sección “Leyendo las hojas de té”, ¿qué te dice esta brecha sobre el modelo y qué parámetro ajustarías para solucionarlo?
Analizar
El artículo advierte que un seasonality_prior_scale alto con datos limitados hace que el modelo “alucine” patrones. Explica paso a paso por qué un martes inusualmente ocupado, combinado con un prior alto, lleva al modelo a tratar cada martes como especial en lugar de como ruido.
Evaluar El artículo recomienda tener dos historiales de Navidad antes de pronosticar una tercera. Critica esa regla general: describe un escenario de negocios en el que dos años de datos aún producirían un pronóstico de días festivos engañoso, y qué información adicional te gustaría tener.
Crear
Diseña una configuración de Prophet para un negocio con un ciclo de festival de música semanal de “cada 4 días”, un día festivo importante de fecha fija (Navidad) y un día festivo de fecha móvil (Pascua). Enumera las elecciones de add_seasonality, holidays y ventanas que tomarías, y justifica cada una.
Artículos relacionados
- Prophet vs. Modelos estadísticos: un enfrentamiento práctico de pronóstico — el artículo anterior: Nora usó Prophet para manejar un quiebre estructural por la apertura de una nueva tienda, con su detección automática de puntos de cambio curvando la tendencia donde ARIMA no podía.
- Validación cruzada de series de tiempo: por qué K-Fold falla con datos temporales — el siguiente artículo: Nora sorprende a un compañero de equipo validando modelos de panadería con K-Fold puro y tiene que explicar por qué eso es hacer trampa en silencio.
Referencias y lecturas adicionales
- Taylor, S. J., & Letham, B. (2018). “Forecasting at Scale.” The American Statistician, 72(1), 37–45. doi.org/10.1080/00031305.2017.1380080 — el artículo fundacional de Prophet; cubre el modelo de descomposición aditiva (tendencia + estacionalidad + días festivos), los términos de estacionalidad de la serie de Fourier y el marco de regresión de días festivos con los efectos de halo
lower_window/upper_windowutilizados en todo este artículo. - Documentación de la biblioteca
holidaysde Python: pypi.org/project/holidays/ — el paqueteholidaysproporciona listas predefinidas de días festivos públicos para 100+ países; la API utilizada aquí (holidays.US(years=[...]),.items()) está documentada en PyPI. - Documentación de la biblioteca
prophet: facebook.github.io/prophet/ — documentación oficial que cubreadd_seasonality,fourier_order,seasonality_prior_scale,holidays_prior_scale, y la APIplot_components. - Competencia de Kaggle “Store Sales — Time Series Forecasting”: kaggle.com/competitions/store-sales-time-series-forecasting — un punto de referencia de pronóstico de ventas minoristas del mundo real con ventas diarias a nivel de tienda, días festivos y múltiples estacionalidades; directamente relevante para probar el enfoque de días festivos con ventana de halo en datos estructuralmente similares a la cadena de panaderías de Nora.
Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .
«Aplica lo que aprendiste» es para suscriptores Supporter e Insider.
Suscríbete para desbloquear los ejercicios de este artículo.
Ver planesArtículos relacionados
- Series Temporales En revisión
Prophet vs. Modelos Estadísticos: Un Enfrentamiento Práctico de Pronósticos
Compara Prophet y ARIMA cara a cara en series de tiempo reales y desordenadas con quiebres estructurales, feriados y múltiples estacionalidades para elegir el modelo correcto.
- Series Temporales En revisión
Pronóstico clásico vs. Machine Learning: ¿Cuándo ARIMA supera a una LSTM?
Aprende por qué ARIMA suele superar a LSTM en pequeños conjuntos de datos de series de tiempo, cuándo usar pronóstico clásico frente a redes neuronales y cómo evitar el sesgo de ajuste en las comparaciones.
- Series Temporales En revisión
Referencia: Anatomía de las series de tiempo
Una referencia sobre la anatomía de las series de tiempo: tendencia, estacionalidad, residuos, autocorrelación y estacionariedad, con ejemplos de descomposición en Python y reglas de decisión.
- Ingeniería en Python En revisión
¿Por qué falla mi pipeline de datos? Una guía amigable para el perfilado de memoria en Python
Aprende a diagnosticar y solucionar los bloqueos por MemoryError en Python en tus pipelines de datos usando memory_profiler, Fil y chunking para gestionar grandes volúmenes de datos con memoria RAM limitada.
¿Buscas otra cosa?
Busca en todos los artículos por título, resumen o tema.