Forecasting con NeuralProphet

Dentro de la ciencia de datos, el forecasting es una técnica ampliamente utilizada a la hora de trabajar en análisis de datos. En este post, veremos qué es y cómo funciona NeuralProphet, una librería de código abierto destinada a llevar a cabo tareas de forecasting.

¿Qué es el forecasting?

En el campo de la estadística y el machine learning, el pronóstico (o forecasting, en inglés) es el proceso de realizar predicciones sobre el futuro basándose en datos del pasado y del presente. Los pronósticos son muy utilizados para la toma de decisiones, ya que hacen posible la definición de una estrategia para ajustarse a las estimaciones realizadas por los modelos. Por otro lado, permiten responder a preguntas sobre el futuro a corto y largo plazo, dependiendo del modelo. Por ejemplo, para modelos meteorológicos:

  • Corto plazo. Para saber si hay que coger un paraguas, podemos preguntar si va a llover en las próximas horas.
  • Medio plazo. A la hora de planificar un viaje, podemos consultar qué tiempo suele hacer en un mes concreto.
  • Largo plazo. Para comprobar que existe el cambio climático, podemos preguntar si la temperatura seguirá aumentando a lo largo de los años.

Los pronósticos se suelen hacer sobre datos de series temporales espaciados en intervalos regulares (por ejemplo, mensual, diario, horario…) y que predicen una variable cuantitativa (por ejemplo, número de ventas, temperatura…). Para estas series temporales, se han identificado 3 patrones que pueden manifestar los datos:

  • Tendencia. Ocurre cuando los datos muestran incrementos o decrementos a largo plazo. Se puede describir como la dirección que lleva la serie temporal.
  • Estacionalidad. Tiene lugar cuando los datos presentan un comportamiento que se repite en períodos fijos dentro de un año. Estos períodos están establecidos por la definición del calendario. Las estacionalidades más comunes son la anual (365 días), la semanal (7 días) y la diaria (24 horas).
  • Ciclo. Sucede cuando los datos revelan un comportamiento que se repite en períodos no fijos. Suelen tener fluctuaciones de más de 2 años. Por ejemplo, la subida o bajada de ventas según la situación económica.

¿Qué es NeuralProphet?

El pronóstico de series temporales es un campo extenso y complejo. Solo para los tres puntos mencionados anteriormente ya existen muchas metodologías para ajustar los datos, cada una con sus hiperparametros a optimizar. Además, hay otros factores como la autocorrelación de los datos (el valor de un día puede estar ligado con un día anterior) que añaden un grado más de dificultad. Por estas razones, muchas veces las personas que conocen los datos y deberían ajustar los modelos no tienen estos modelos a su alcance. 

Prophet es una librería que pretende simplificar el proceso de pronóstico para llevarlo al alcance de más gente y que se pueda utilizar en muchos marcos diferentes. El modelo propuesto por Prophet presenta valores de hiperparámetros por defecto que se ajustan a muchas series temporales y evalúan automáticamente el modelo.

La librería Prophet fue desarrollada por Meta y lanzada como open source en 2017. Dicha librería describe una serie temporal en función de tres componentes: tendencia, estacionalidad y festivos (o eventos). Este tipo de descomposición es típico en los modelos de pronóstico, ya que permite separar la complejidad de la serie temporal en partes independientes con un significado muy claro.

Inconvenientes de Prophet

Una de las principales críticas a Prophet es que el entrenamiento del modelo se hace ajustando la mejor curva (como con una regresión lineal) y no aprende la relación entre un punto temporal y el siguiente. En otros casos, esto se trata, por ejemplo, con modelos autoregresivos (AR). Este problema se vio claramente con la pandemia del COVID-19. Muchas series temporales experimentaron bajadas bruscas y un cambio de comportamiento. Si bien el modelo mejoraba al tener más datos en la nueva situación y pudiendo describir variaciones en la tendencia, el cambio brusco hacía inviable el pronóstico para esas fechas.

La siguiente imagen muestra un ejemplo en la página oficial del ajuste para tráfico peatonal en Melbourne. En negro, los datos reales y, en azul, el ajuste de un modelo de Prophet. Se puede observar el efecto del COVID-19.

Ejemplo de modelo con Prophet para tráfico peatonal en Melbourne

Por estas razones, en 2021 sale una nueva librería, NeuralProphet, que añade 3 nuevos componentes a las de Prophet: autoregresión (valores pasados de la variable a predecir), regresores a pasado (otras variables en el pasado) y regresores a futuro (otras variables en el futuro). Estos 3 nuevos componentes utilizan un número de puntos temporales a pasado o a futuro a definir. Los 2 primeros se entrenan mediante redes neuronales.

Preprocesado y entrenamiento con NeuralProphet

La librería tiene configurados algunos procesos de preprocesamiento por defecto. En concreto, utiliza diferentes técnicas para imputar valores nulos según la cantidad de puntos consecutivos a rellenar. También realiza una normalización de los datos que puede ser personalizada por el usuario.

Para garantizar que el ajuste se realice correctamente, la serie temporal se tabulariza y se crea un dataset pseudoindependiente. El modelo se entrena utilizando PyTorch y siguiendo un descenso de gradiente estocástico (SGD). Adicionalmente, NeuralProphet tiene predefinidos los siguientes componentes y hiperparámetros, aunque pueden ser modificados por el usuario:

  • Función de pérdida. Se utiliza la función de Huber (L1 suavizada).
  • Regularización. El log del absoluto de los pesos se escala y se traslada.
  • Optimizador. Uso del optimizador AdamW.
  • Tasa de aprendizaje. Se realiza para obtener test de la tasa óptima 3 veces y se hace una media.
  • Tamaño del lote. Un valor entre 16 y 256 dependiendo linealmente de los puntos disponibles en la serie temporal. En caso de tener menos de 16 puntos, se utilizan todos los puntos disponibles.
  • Épocas de entrenamiento. Un número entre 50 y 500 dependiendo inversamente del número de puntos de la serie temporal.

Componentes de NeuralProphet

El modelo de NeuralProphet se puede describir como la suma de los componentes mencionados anteriormente:

ŷt =T(t)+S(t)+A(t)+L(t)+F(t)+E(t)= T(t) + S(t)+ A(t) + L(t) + F(t) + E(t)

donde para un instante t:

  • T(t) es la tendencia.
  • S(t) es la estacionalidad.
  • A(t) es el efecto de autoregresión .
  • L(t) es el efecto de regresión para variables con solo pasado conocido.
  • F(t) es el efecto de regresión para variables con futuro conocido.
  • E(t) es el efecto de los eventos y las vacaciones.

También se pueden configurar estos componentes para que se combinen de manera multiplicativa, en vez de aditiva. La configuración óptima depende del proceso que se quiere describir.

NeuralProphet pronostica un número de puntos a futuro a la vez. Esto es un parámetro configurable llamado horizonte (h). Todos los componentes predicen h puntos.

Dataset de ejemplo

En los siguientes apartados, se abordarán algunos ejemplos utilizando datos de calidad de aire de España y datos meteorológicos de AEMET Open Data para intentar predecir la concentración diaria media de NO2 en Palma. Se han empleado datos de los años 2017-2022 (ambos años incluidos).

Los datos horarios de calidad del aire se han filtrado por la provincia 7 (Baleares), municipio 40 (Palma) y estación 2 (avenidas) para obtener una sola estación y seleccionando la magnitud 8, que corresponde a NO2. Después, se ha realizado un promedio por todas las horas de cada día. De esta manera, se obtienen datos diarios de concentración de NO2 media para una estación de Palma. Tras la lectura, estas son las transformaciones:

# Filter for traffic station in Palma (Avenidas)
df_air = df_air.loc[
    (df_air.PROVINCIA == 7)
    & (df_air.MUNICIPIO == 40)
    & (df_air.ESTACION == 2)
].copy()

# Get columns
df_air['ds'] = pd.to_datetime(df_air.apply(
    lambda s: f"{s['ANNO']:04d}-{s['MES']:02d}-{s['DIA']:02d}", 
    axis=1
))
df_air['y'] = df_air[[f"H{h:02d}" for h in range(1, 25)]] \
    .mean(axis=1)

Los datos meteorológicos se han filtrado por la estación B278 (aeropuerto de Palma). Se han obtenido métricas diarias de temperatura media (tmed), precipitación (prec), viento medio (velmedia), humedad media (hrMedia) y horas de sol (sol). Después de la lectura, estas son las transformaciones:

df_weather['ds'] = pd.to_datetime(df_weather['fecha'])

# Ip is <0,1
df_weather['prec'] = np.where(
    df_weather['prec'] == 'Ip', 
    '0,1',
    df_weather['prec']
)

float_cols = ['tmed', 'prec', 'velmedia', 'hrMedia', 'sol']
for c in float_cols:
    df_weather[c] = df_weather[c].astype('str') \
        .str.replace(',', '.').astype('float')

Finalmente, se juntan los datos de los dos orígenes:

# All dates
df = pd.date_range("2017-01-01", "2022-12-31") \
    .rename('ds').to_frame().reset_index(drop=True) \
    .merge(df_air, on='ds', how='left') \
    .merge(df_weather, on='ds', how='left')

cols = ['ds', 'y', 'tmed', 'velmedia', 'prec', 'hrMedia']
df = df[cols].sort_values('ds') \
    .reset_index(drop=True).copy()

DS_SPLIT_VALID = datetime(2022, 1, 1)
DS_SPLIT_TEST = datetime(2022, 7, 1)

df_train = df.loc[df.ds < DS_SPLIT_VALID]
df_valid = df.loc[
    (df.ds >= DS_SPLIT_VALID) 
    & (df.ds < DS_SPLIT_TEST)
]
df_test = df.loc[df.ds >= DS_SPLIT_TEST]

También se crean dataframes de entrenamiento, validación y testeo. Como nota, para utilizar NeuralProphet, se requiere un dataframe de la librería Pandas en Python que tenga principalmente dos columnas. Por un lado, ds (la fecha en tipo datetime) y, por otro, y (el valor a predecir).

Si se representa un gráfico del dataframe resultante, se observa la siguiente serie temporal:

Ejemplo de representación gráfica de una serie temporal de dataframe con NeuralProphet

Tal y como podemos ver, se observan dos rangos de fechas en los que no está informado NO2: alrededor de marzo de 2018 y agosto de 2022. También se puede advertir el efecto de la pandemia COVID-19.

Tendencia

Una manera clásica de tratar la tendencia es utilizar una línea recta, siendo la pendiente la tasa de crecimiento. NeuralProphet parte de esta idea e introduce el concepto de puntos de cambio (changepoints), que son momentos en el tiempo en el que la tasa de crecimiento cambia. Los puntos de cambio se pueden configurar o pueden ser configurados automáticamente por el modelo. El resultado es una tendencia compuesta por varias rectas con diferentes pendientes unidas en los puntos de cambio.

En el siguiente ejemplo, se ha ajustado el modelo de NeuralProphet solo con la tendencia.

cols = ['ds', 'y']
m = NeuralProphet(
    weekly_seasonality=False,
    yearly_seasonality=False
)
m.fit(df_train[cols], validation_df=df_valid[cols])

A continuación, se hacen predicciones sobre todos los datos y se compara en un gráfico con los datos reales gráficos para ver el ajuste:

forecast = m.predict(df[cols])
fig = m.plot(forecast, figsize=(14,8))

# ds split line
fig.add_vline(x=DS_SPLIT_VALID.timestamp()*1000,
              line_dash='dash',
              annotation_text='Validation')
fig.add_vline(x=DS_SPLIT_TEST.timestamp()*1000,
              line_dash='dash',
              annotation_text='Test')

# Prediction on top 
fig.data = fig.data[::-1]
fig
Representación gráfica de tendencia en NeuralProphet

En el gráfico se observa que la predicción es un conjunto de rectas unidas. La tendencia da una idea genérica de en qué dirección va la serie temporal. Exceptuando un cambio de tendencia durante la época del COVID-19, no parece variar mucho. Ejecutando plot_parameters se pueden ver los checkpoints y los cambios en la tendencia:

m.plot_parameters(figsize=(14,4))
Representación gráfica de cambio de tendencia con plot_parameters en NeuralProphet

Estacionalidad

El componente de estacionalidad en NeuralProphet se define como una serie de Fourier de k términos. Se pueden configurar más de una periodicidad (p) a la vez, ajustándose una serie de Fourier para cada una. El efecto total de la estacionalidad es la suma de todas ellas.

Si no se configura nada, el modelo analiza la escala de tiempo de los datos (mensuales, diarios, horarios, etc.) y si presentan periodicidad para aquellas periodicidades posibles, mirando aquellas periodicidades más pequeñas que la escala de tiempo y con suficientes datos. Además, para cada periodicidad, el modelo define un número de términos de Fourier k que funciona bien. Alternativamente, se puede configurar manualmente estas dos partes.

A continuación, se coge el modelo anterior y se activa la estacionalidad automática. El modelo de este ejemplo es equivalente a instanciar NeuralProphet sin ningún parámetro, ya que por defecto ajusta una tendencia y las estacionalidades posibles.

cols = ['ds', 'y']
m = NeuralProphet()
m.fit(df_train[cols], validation_df=df_valid[cols])

De nuevo, se muestra el gráfico con el ajuste del modelo. El modelo se parece al anterior, pero ahora las rectas son curvas oscilantes que se intentan acercar más a las variaciones de los datos.

Representación gráfica de estacionalidad en NeuralProphet

La estacionalidad semanal casi no se puede observar a esta escala. No obstante, se puede ver cómo las oscilaciones se repiten en un mismo momento del año. Por ejemplo, justo antes de fin de año parece haber un pico en esta estacionalidad. Esto podría coincidir con la época navideña. Para ver mejor cada componente, se vuelve a representar el gráfico de parámetros:

Representación gráfica de oscilación de estacionalidad en NeuralProphet

Autoregresión

El componente de autorregresión trata de describir la variable dependiente (yt) en función de valores pasados de ella misma (yt-1, yt-2, etc.). NeuralProphet se basa en el modelo AR-Net para evaluar este componente. Este modelo utiliza redes neuronales en Pytorch para poder encontrar relaciones complejas entre los valores pasados que permitan hacer una predicción. La red neuronal tiene un número de inputs igual al número de puntos a pasado que se quieran utilizar y una cantidad de outputs igual al horizonte (h).

cols = ['ds', 'y']
m = NeuralProphet(n_lags=1)
m.fit(df_train[cols], validation_df=df_valid[cols])
Representación gráfica de autoregresión en NeuralProphet

A continuación, se vuelven a representar los parámetros. Aquí el comando cambia un poco para indicar que, en caso de haber más de un lag, se muestre el primero:

m.plot_parameters(
    figsize=(14,4), 
    components=['autoregression'], 
    forecast_in_focus=1
)
Representación gráfica de autoregresión con lag en NeuralProphet

Regresores a pasado

El componente de regresión para variables a pasado trata de describir la variable dependiente (yt) en función de los valores pasados de otras variables independientes (xt-1, xt-2, etc.). Cada una de estas variables se ajusta volviendo a utilizar AR-Net. La única diferencia con el componente de autoregresión es la variable de entrada de la red neuronal.

Una hipótesis puede ser que el viento es capaz de mover el NO2, teniendo un peso sobre su concentración. A continuación, se añade el nuevo regresor velmedia al modelo anterior y configurando un solo lag, al igual que el componente de autoregresión. 

cols = ['ds', 'y', 'velmedia']
m = NeuralProphet(n_lags=1)
m.add_lagged_regressor("velmedia", n_lags=1)
m.fit(df_train[cols], validation_df=df_valid[cols])

A continuación, se muestra el resultado y los parámetros:

Representación gráfica de regresor a pasado en NeuralProphet
Representación gráfica de regresor a pasado con lag en NeuralProphet

Tal y como se puede observar, vemos un peso relativamente pequeño de esta variable en comparación al componente de autoregresión.

Regresores a futuro

El componente de regresión para variables con futuro conocido trata de describir la variable dependiente (yt) en función de los valores pasados y futuros de variables independientes. Estas variables deben ser pronosticadas por otros modelos o conocidas de antemano en el momento de predecir yt.

En este ejemplo, se añade la información de temperatura media. Al ser un regresor a futuro, se está asumiendo que se dispone de un sistema externo de pronóstico de la temperatura media.

cols = ['ds', 'y', 'velmedia', 'tmed']
m = NeuralProphet(n_lags=1)
m_future_regressors.add_lagged_regressor("velmedia", n_lags=1)
m_future_regressors.add_future_regressor("tmed")
m.fit(df_train[cols], validation_df=df_valid[cols])

El modelo se entrena y se obtienen los siguientes gráficos:

Representación gráfica de regresor a futuro en NeuralProphet
Representación gráfica de regresor a futuro con lag en NeuralProphet

Eventos

El componente de eventos y vacaciones trata de describir el impacto que tienen ciertas fechas sobre la variable dependiente (yt). Para vacaciones específicas de un país, NeuralProphet integra la librería python-holidays, de tal manera que solo hace falta especificar el país deseado. El usuario también puede definir eventos personalizados como conciertos, partidos de fútbol o clima extremo, definiendo las fechas en las que ocurre. También se puede definir una ventana de tiempo alrededor de los eventos para indicar que los días adyacentes pueden tener efecto.

Para el caso de uso, se añaden las vacaciones de España:

cols = ['ds', 'y', 'velmedia', 'tmed']
m = NeuralProphet(n_lags=1)
m_future_regressors.add_lagged_regressor("velmedia", n_lags=1)
m_future_regressors.add_future_regressor("tmed")
m.add_country_holidays('ES')
m.fit(df_train[cols], validation_df=df_valid[cols])
Representación gráfica de evento personalizado en NeuralProphet
Gráfico de barras de evento personalizado en NeuralProphet

Se puede observar un efecto negativo de todas las vacaciones sobre la concentración de NO2.

Comparativa

En los ejemplos anteriores se han ido añadiendo componentes incrementalmente y evaluando visualmente los resultados. A continuación, se expande el modelo de 2 formas:

  • Por un lado, se incrementa el número de lags de autoregresión y de las variables a pasado de 1 a 3.
  • Por otra parte, se añaden las variables a pasado hrMedia y prec.

El código entero es el resultante:

cols = ['ds', 'y', 'tmed', 'velmedia', 'prec', 'hrMedia']
m = NeuralProphet(n_lags=3)
m.add_lagged_regressor("velmedia", n_lags=3)
m.add_lagged_regressor("hrMedia", n_lags=3)
m.add_lagged_regressor("prec", n_lags=3)
m.add_future_regressor("tmed")
m.add_country_holidays("ES")
m.fit(df_train[cols], validation_df=df_valid[cols])
Gráfico de barras de evento personalizado en NeuralProphet

Finalmente, para tener un resultado numérico, se utiliza el método test para evaluar una serie de métricas. Se escoge el RMSE y se compara con los modelos anteriores:

T(t)S(t)AR(P)(t)L(P)(t)F(t)E(t)RMSE
linear–––––9.47
linearyearly
weekly
––––8.86
linearyearly
weekly
AR(1)–––7.13
linearyearly
weekly
AR(1)velmedia(1)––7.10
linearyearly
weekly
AR(1)velmedia(1)tmed–7.17
linearyearly
weekly
AR(1)velmedia(1)tmedES7.01
linearyearly
weekly
AR(3)velmedia(3)
hrMedia(3)
prec(3)
tmedES6.84

Conclusión

NeuralProphet es una librería de forecasting diseñada para ser simple. A lo largo de este post, se ha comprobado cómo con poca configuración se obtienen resultados decentes gracias a que la librería ajusta varios parámetros por detrás. La separación en diferentes componentes permite describir diferentes comportamientos típicos de las series temporales.

Si este artículo te ha parecido interesante, te animamos a visitar la categoría Data Science para ver otros posts similares a este y a compartirlo en redes. ¡Hasta pronto!

Guillermo Camps
Guillermo Camps
Artículos: 22