El pronóstico de series temporales implica la creación de modelos a partir de datos históricos con marca de tiempo para realizar predicciones científicas e impulsar la toma de decisiones estratégicas en el futuro. El pronóstico de series de tiempo tiene muchos usos en varios dominios, que incluyen:
El pronóstico de series temporales es un poco diferente del caso de uso de aprendizaje automático tradicional, ya que implica un ordenamiento temporal de los datos que deben tenerse en cuenta durante la ingeniería y el modelado de características.
Para entrenar un modelo de pronóstico de series temporales, termina en una situación en la que usa Pandas para el preprocesamiento, statsmodel para estacionalidad y pruebas estadísticas, scikit-learn o Facebook Prophet para pronósticos y código personalizado para implementar backtesting y selección de modelos.
La previsión de series temporales de un extremo a otro se convierte en una tarea tediosa para los científicos de datos, ya que las diferentes bibliotecas tienen distintas API y tipos de datos. Para los casos de uso de aprendizaje automático tradicionales, tenemos el paquete scikit-learn, que proporciona una API coherente para el modelado de aprendizaje automático de extremo a extremo.
Darts intenta ser un scikit-learn para series de tiempo, y su objetivo principal es simplificar todo el enfoque de pronóstico de series de tiempo. En este artículo, discutiremos el paquete de dardos y su implementación.
Darts es una biblioteca de Python para una fácil manipulación y pronóstico de series temporales. Ofrece implementaciones de una variedad de modelos, desde clásicos como ARIMA hasta redes neuronales profundas, que se pueden implementar de la misma manera que los modelos scikit-learn (usando API de ajuste y predicción).
Algunas de las características del paquete Darts son:
Está construido alrededor de la clase inmutable TimeSeries
.
Tiene una interfaz API unificada y fácil de usar similar a scikit-learn como fit() y predict().
Ofrece una variedad de modelos, desde modelos clásicos hasta enfoques ML/DL de última generación. Proporciona API para casos de uso de pronóstico de series temporales de extremo a extremo, incluido el descubrimiento de datos, el preprocesamiento de datos, el pronóstico y la selección y evaluación de modelos.
Aquí solo mostraremos algunos ejemplos mínimos de "inicio". Para obtener información más detallada, puede consultar la guía de usuario y cuadernos de ejemplo.
Recomendamos utilizar algún entorno virtual. Entonces hay principalmente dos formas.
Con pip:
pip install darts
Con conda
conda install -c conda-forge -c pytorch u8darts-all
consulte la guía detallada de instalación si tiene problemas o desea instalar un sabor diferente (evitando ciertas dependencias).
Primero, importemos algunas cosas:
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from darts import TimeSeries
from darts.datasets import AirPassengersDataset
TimeSeries
¶TimeSeries
es la clase de datos principal en Darts. Una 'TimeSeries' representa una serie temporal univariante o multivariante, con un índice de tiempo adecuado. El índice de tiempo puede ser del tipo pandas.DatetimeIndex
(que contiene fechas y horas) o del tipo pandas.RangeIndex
(que contiene números enteros; útil para representar datos secuenciales sin marcas de tiempo específicas). En algunos casos, TimeSeries
puede incluso representar series probabilísticas, para, por ejemplo, obtener intervalos de confianza. Todos los modelos en Darts consumen TimeSeries
y producen TimeSeries
.
TimeSeries
¶Desde un Pandas DataFrame completo, usandoTimeSeries
se puede construir fácilmente usando algunos métodos de fábrica:
TimeSeries.from_dataframe()
(docs).TimeSeries.from_times_and_values()
(docs).TimeSeries.from_values()
(docs).Series
de Pandas, usando TimeSeries.from_series()
(docs).xarray.DataArray
, usando TimeSeries.from_xarray()
(docs).TimeSeries.from_csv()
(docs).A continuación, obtenemos una 'TimeSeries' al cargar directamente la serie de pasajeros aéreos de uno de los conjuntos de datos disponibles en Darts:
series = AirPassengersDataset().load()
series.plot()
TimeSeries
¶TimeSeries
soporta diferentes tipos de operaciones - aquí hay algunos ejemplos.
También podemos dividir en una fracción de la serie, en un Timestamp
de pandas o en un valor de índice entero.
series1, series2 = series.split_before(0.75)
series1.plot()
series2.plot()
series1, series2 = series[:-36], series[-36:]
series1.plot()
series2.plot()
series_noise = TimeSeries.from_times_and_values(
series.time_index, np.random.randn(len(series))
)
(series / 2 + 20 * series_noise - 10).plot()
Concatenar una nueva dimensión para producir una nueva serie única multivariante.
(series / 50).stack(series_noise).plot()
series.map(np.log).plot()
series.map(lambda ts, x: x / ts.days_in_month).plot()
(series / 20).add_datetime_attribute("month").plot()
(series / 200).add_holidays("US").plot()
Diferenciar una serie de tiempo implica calcular la diferencia entre valores consecutivos de la serie. Esto se hace para eliminar o reducir la tendencia y la estacionalidad presentes en la serie, dejando una serie estacionaria. La diferenciación es una técnica común utilizada en el análisis de series de tiempo para lograr la estacionariedad y hacer que la serie sea más fácil de modelar y predecir.
El proceso de diferenciación se realiza restando el valor de un período anterior al valor del período actual. Esto se puede hacer de diferentes órdenes, dependiendo de la cantidad de diferenciación requerida para hacer que la serie sea estacionaria. La serie resultante después de la diferenciación se conoce como la primera diferencia, y se puede repetir el proceso para obtener la segunda diferencia, la tercera diferencia, y así sucesivamente.
La diferenciación de una serie de tiempo puede ayudar a cumplir dos objetivos principales:
Eliminar la tendencia: La tendencia se refiere a la dirección general y persistente en la serie de tiempo a lo largo del tiempo. Al restar los valores consecutivos, se pueden eliminar o reducir las fluctuaciones de la tendencia, lo que permite analizar y modelar más fácilmente las variaciones estacionarias.
Eliminar la estacionalidad: La estacionalidad se refiere a patrones regulares y recurrentes en la serie de tiempo que se repiten en intervalos específicos. Al diferenciar la serie, se pueden eliminar o reducir los efectos de la estacionalidad, lo que facilita la identificación de patrones más sutiles o residuales en la serie.
La diferenciación de series de tiempo es una técnica común en el análisis y pronóstico de datos de series temporales. Puede ser utilizada en combinación con otros métodos, como modelos autoregresivos (AR), modelos de promedio móvil (MA) o modelos autorregresivos de promedio móvil (ARMA), para lograr una mejor comprensión y predicción de las series de tiempo.
series.diff().plot()
utils
¶Los valores faltantes están representados por np.nan
.
from darts.utils.missing_values import fill_missing_values
values = np.arange(50, step=0.5)
values[10:30] = np.nan
values[60:95] = np.nan
series_ = TimeSeries.from_values(values)
(series_ - 10).plot(label="con valores faltantes (desplazados abajo)")
fill_missing_values(series_).plot(label="sin valores faltantes")
Para lo que sigue, dividiremos nuestra TimeSeries
en una serie de entrenamiento y una de validación.
Nota:
En general, también es una buena práctica mantener a un lado una serie de pruebas y nunca tocarla hasta el final del proceso. Aquí, solo creamos una serie de entrenamiento y una de validación para simplificar.
La serie de entrenamiento será una TimeSeries
que contenga valores hasta enero de 1958 (excluidos), y la serie de validación una TimeSeries
que contenga el resto:
train, val = series.split_before(pd.Timestamp("19580101"))
train.plot(label="entrenamiento")
val.plot(label="validación")
Hay una colección de modelos de línea de base "ingenuos" en Darts, que pueden ser muy útiles para tener una idea de la precisión mínima que uno podría esperar. Por ejemplo, el modelo NaiveSeasonal(K)
siempre "repite" el valor que ocurrió hace K
pasos de tiempo.
En su forma más ingenua, cuando K=1
, este modelo simplemente siempre repite el último valor de la serie de entrenamiento:
from darts.models import NaiveSeasonal
naive_model = NaiveSeasonal(K=1)
naive_model.fit(train)
naive_forecast = naive_model.predict(36)
series.plot(label="real")
naive_forecast.plot(label="pronóstico ingenuo (K=1)")
Es muy fácil ajustar modelos y producir predicciones en TimeSeries
. Todos los modelos tienen una función fit()
y predict()
. Esto es similar a Scikit-learn, excepto que es específico para series temporales. La función fit()
toma como argumento la serie de tiempo de entrenamiento en la que se ajusta el modelo, y la función predict()
toma como argumento el número de pasos de tiempo (después del final de la serie de entrenamiento) sobre los cuales se pronostica .
Nuestro modelo anterior es quizás demasiado ingenuo. Ya podemos mejorar explotando la estacionalidad de los datos. Parece bastante obvio que los datos tienen una estacionalidad anual, lo que podemos confirmar observando la función de autocorrelación (ACF) y resaltando el retraso m=12
:
from darts.utils.statistics import plot_acf, check_seasonality
plot_acf(train, m=12, alpha=0.05)
El ACF (Autocorrelation Function) en series de tiempo es una función que muestra la correlación entre los valores pasados de una serie de tiempo y los valores actuales. En otras palabras, el ACF muestra cómo se relaciona cada observación en una serie de tiempo con las observaciones anteriores en diferentes retrasos o desfases.
El ACF se calcula mediante el coeficiente de correlación de Pearson entre la serie original y una versión desplazada de sí misma. Para cada retraso o desfase, se calcula el coeficiente de correlación y se representa en un gráfico de barras o en una función de autocorrelación.
El gráfico del ACF es útil para analizar la estructura de correlación en una serie de tiempo y puede proporcionar información sobre la estacionalidad, la tendencia y otros patrones presentes en los datos. Algunos puntos clave al interpretar el gráfico del ACF son:
Si el ACF muestra una correlación significativa en el primer desfase (lag 1), indica que los valores adyacentes en la serie de tiempo están correlacionados. Esto sugiere una posible dependencia serial en los datos.
Si el ACF muestra una autocorrelación significativa en múltiples desfases, sugiere que la serie de tiempo puede tener un patrón estacional o una tendencia autoregresiva.
Si el ACF muestra autocorrelaciones que van disminuyendo gradualmente a medida que aumenta el desfase, indica que la serie de tiempo puede seguir un modelo de decaimiento exponencial o un modelo autoregresivo de orden finito (AR).
Si el ACF muestra una autocorrelación significativa solo en el desfase 0 (lag 0) y es cercana a cero para todos los demás desfases, sugiere que la serie de tiempo es ruidosa o aleatoria sin correlación serial.
El análisis del ACF puede ayudar a seleccionar el orden adecuado para modelos de series de tiempo, como modelos AR, modelos de promedio móvil (MA) o modelos autorregresivos de promedio móvil (ARMA). También puede proporcionar información útil para el diagnóstico y ajuste de modelos de series de tiempo.
El ACF presenta un pico en x = 12, lo que sugiere una tendencia de estacionalidad anual (resaltada en rojo). La zona azul determina la importancia de las estadísticas para un nivel de confianza de 𝛼=5% . También podemos realizar una verificación estadística de estacionalidad para cada período candidato m:
for m in range(2, 25):
is_seasonal, period = check_seasonality(train, m=m, alpha=0.05)
if is_seasonal:
print("Hay estacionalidad de orden {}.".format(period))
Hay estacionalidad de orden 12.
Probemos de nuevo el modelo NaiveSeasonal
con una estacionalidad de 12:
seasonal_model = NaiveSeasonal(K=12)
seasonal_model.fit(train)
seasonal_forecast = seasonal_model.predict(36)
series.plot(label="real")
seasonal_forecast.plot(label="pronóstico ingenuo (K=12)")
Esto es mejor, pero aún nos falta la tendencia. Afortunadamente, también hay otro modelo de referencia ingenuo que captura la tendencia, que se llama NaiveDrift
. Este modelo simplemente produce predicciones lineales, con una pendiente determinada por el primer y el último valor del conjunto de entrenamiento:
from darts.models import NaiveDrift
drift_model = NaiveDrift()
drift_model.fit(train)
drift_forecast = drift_model.predict(36)
combined_forecast = drift_forecast + seasonal_forecast - train.last_value()
series.plot()
combined_forecast.plot(label="combinado")
drift_forecast.plot(label="tendencia")
¿Que paso ahi? Simplemente ajustamos un modelo de tendencia ingenuo y agregamos su pronóstico al pronóstico estacional que teníamos anteriormente. También restamos el último valor del conjunto de entrenamiento del resultado, de modo que el pronóstico combinado resultante comience con la compensación correcta.
Esto ya parece un pronóstico bastante decente, y todavía no usamos ningún modelo no ingenuo. De hecho, cualquier modelo debería poder superar esto.
Entonces, ¿cuál es el error que tendremos que vencer? Usaremos el Error porcentual absoluto medio (MAPE) (tenga en cuenta que en la práctica a menudo hay buenas razones para no usar el MAPE; lo usamos aquí ya que es bastante conveniente e independiente de la escala). En Darts es una llamada de función simple:
from darts.metrics import mape
print(
'Error porcentual absoluto medio para la tendencia ingenua combinada + estacional: {:.2f}%.'.format(
mape(series, combined_forecast)
)
)
Error porcentual absoluto medio para la tendencia ingenua combinada + estacional: 5.66%.
darts.metrics
contiene muchas más métricas para comparar series temporales. Las métricas compararán solo segmentos comunes de series cuando las dos series no estén alineadas y paralelizarán el cálculo en una gran cantidad de pares de series, pero no nos adelantemos.
Darts está diseñado para facilitar el entrenamiento y la validación de varios modelos de forma unificada. Entrenemos algunos más y calculemos su MAPE respectivo en el conjunto de validación. Antes describimos muy brevemente los modelo que probaremos.
El suavizamiento exponencial es una técnica utilizada en el análisis de series de tiempo para predecir valores futuros o suavizar las fluctuaciones de una serie. Es un método de pronóstico que asigna pesos exponenciales decrecientes a los valores pasados de la serie, dando más importancia a los valores más recientes.
Existen diferentes variantes de suavizado exponencial, las más comunes son:
Suavizado exponencial simple (SES): En este método, se calcula una media ponderada exponencialmente de los valores pasados de la serie. Cada observación recibe un peso que disminuye exponencialmente a medida que se retrocede en el tiempo. La fórmula general del suavizado exponencial simple es:
Donde:
Suavizado exponencial doble (Holt): Este método extiende el suavizado exponencial simple al considerar tanto la tendencia como el nivel en la serie de tiempo. Se utiliza cuando la serie de tiempo muestra una tendencia constante. La fórmula general del suavizado exponencial doble es:
Donde:
Suavizado exponencial triple (Holt-Winters): Este método incorpora no solo la tendencia, sino también los componentes estacionales en la serie de tiempo. Es adecuado para series de tiempo que exhiben patrones estacionales repetitivos. Además de las ecuaciones del suavizado exponencial doble, se agregan componentes estacionales que se actualizan utilizando un factor de suavizado γ.
La variante aditiva de Holt-Winters se utiliza cuando las fluctuaciones estacionales son relativamente constantes a lo largo de la serie. La variante multiplicativa se utiliza cuando las fluctuaciones estacionales cambian proporcionalmente a los niveles de la serie.
El suavizado exponencial se basa en la idea de que los valores más recientes tienen más influencia en las predicciones futuras, y los valores más antiguos tienen menos peso. A través de diferentes variantes, el suavizado exponencial puede adaptarse a diferentes patrones de series de tiempo y proporcionar pronósticos más precisos.
TBATS (Trigonometric seasonality, Box-Cox transformation, ARMA errors, Trend and Seasonal components) es un modelo estadístico utilizado para el análisis y pronóstico de series de tiempo que incorpora múltiples componentes, como la estacionalidad, la tendencia y los errores autorregresivos de media móvil (ARMA).
El modelo TBATS es una extensión del modelo de suavizado exponencial triple (Holt-Winters) que se adapta a las series de tiempo con patrones estacionales complejos y no lineales. Al incorporar componentes trigonométricas, como funciones seno y coseno, el modelo puede capturar estacionalidades con frecuencias múltiples y no necesariamente constantes.
Además de la estacionalidad, el modelo TBATS también puede manejar tendencias no lineales mediante la inclusión de términos polinómicos para modelar cambios en la pendiente o la curvatura de la serie de tiempo.
El modelo TBATS también incluye una transformación Box-Cox para manejar la variabilidad heterocedástica de la serie de tiempo y estabilizar la varianza.
En resumen, el modelo TBATS es un enfoque flexible y potente para el análisis y pronóstico de series de tiempo que combina diferentes componentes, como la estacionalidad, la tendencia, la transformación Box-Cox y los errores ARMA. Es especialmente útil para series de tiempo con patrones estacionales complejos y no lineales.
El modelo ARIMA (Autoregressive Integrated Moving Average) es un modelo estadístico utilizado para analizar y predecir series de tiempo. Combina componentes de autoregresión (AR), promedio móvil (MA) y diferenciación (I) para capturar tanto la dependencia en el tiempo como la estacionalidad presente en los datos.
Cada componente del modelo ARIMA tiene un propósito específico:
Componente de Autoregresión (AR): Representa la dependencia lineal de los valores pasados de la serie de tiempo. La idea detrás del AR es que el valor actual de la serie de tiempo puede explicarse mediante una combinación lineal de sus valores pasados. El orden del AR, denotado como p, indica cuántos valores pasados se utilizan en la predicción del valor actual.
Componente de Promedio Móvil (MA): Representa la dependencia lineal de los errores residuales pasados en la serie de tiempo. Los errores residuales son las diferencias entre los valores reales y los valores predichos por el modelo AR. El orden del MA, denotado como q, indica cuántos errores residuales pasados se tienen en cuenta para predecir el valor actual.
Componente de Diferenciación (I): Se utiliza para eliminar tendencias y estacionalidades presentes en la serie de tiempo. La diferenciación implica tomar la diferencia entre los valores observados en diferentes períodos. El orden de la diferenciación, denotado como d, indica el número de veces que se realiza esta operación para estabilizar la varianza y hacer que la serie de tiempo sea estacionaria.
La fórmula general de un modelo ARIMA (p,d,q) se puede expresar de la siguiente manera:
ΔdY(t)=c+Σ(φi∗ΔdY(t−i))+Σ(θj∗ϵ(t−j))+ϵ(t)Donde:
En esta fórmula, los coeficientes φi y θj se estiman a partir de los datos de la serie de tiempo. Los valores de p,dyq indican los órdenes correspondientes del modelo ARIMA. El valor de p representa el orden del componente AR, el valor de d indica el número de diferenciaciones realizadas y el valor de q representa el orden del componente MA.
Es importante tener en cuenta que la forma exacta de la fórmula puede variar dependiendo de la implementación y las convenciones utilizadas en el software o el contexto específico del análisis de series de tiempo.
AUTOARIMA implementa un algoritmo automático para ajustar modelos ARIMA (AutoRegressive Integrated Moving Average) a series de tiempo. El método AUTOARIMA busca automáticamente el mejor modelo ARIMA para una serie de tiempo determinada, teniendo en cuenta los patrones y características de la serie.
El algoritmo de AUTOARIMA utiliza un enfoque heurístico para seleccionar los parámetros p, d y q del modelo ARIMA, que corresponden a los órdenes de autoregresión, la diferenciación y el promedio móvil, respectivamente. El enfoque se basa en una combinación de técnicas de búsqueda y criterios de selección de modelos, como el criterio de información de Akaike (AIC) o el criterio de información bayesiano (BIC).
La función AUTOARIMA examina diferentes combinaciones de órdenes p, d y q y evalúa la bondad de ajuste de los modelos correspondientes mediante los criterios de selección mencionados anteriormente. Luego, selecciona el modelo que minimiza el criterio elegido, lo que indica que es el modelo más adecuado para la serie de tiempo dada.
AUTOARIMA también puede tener en cuenta la estacionalidad en la serie de tiempo utilizando modelos SARIMA (Seasonal ARIMA), que incorporan componentes estacionales adicionales. El algoritmo puede buscar y seleccionar automáticamente los órdenes estacionales p, d, q y s.
En resumen, AUTOARIMA es un algoritmo proporciona una manera automatizada y conveniente de ajustar modelos ARIMA a series de tiempo, seleccionando automáticamente los órdenes óptimos del modelo basándose en criterios de selección. Esto facilita el proceso de modelado y pronóstico de series de tiempo para aquellos que no tienen experiencia en la selección manual de órdenes ARIMA.
El modelo Theta(θ) en el contexto de las series de tiempo se refiere a un modelo específico utilizado para el análisis y pronóstico de datos de series temporales. También se conoce como modelo de caminata aleatoria o modelo de primer orden autorregresivo.
El modelo Theta es relativamente simple y se basa en la premisa de que el valor futuro de la serie de tiempo es igual al valor actual más un componente de ruido aleatorio. En otras palabras, la serie de tiempo sigue una trayectoria similar a una caminata aleatoria.
La ecuación básica del modelo Theta se puede expresar de la siguiente manera:
Donde:
En este modelo, el valor futuro de la serie de tiempo se estima simplemente sumando un término de ruido aleatorio al valor actual. No hay consideración de patrones de tendencia, estacionalidad o autocorrelación en la serie.
El modelo Theta puede ser útil en situaciones donde la serie de tiempo no muestra patrones claros de tendencia o estacionalidad y se espera que los cambios futuros sean impulsados principalmente por el azar o factores aleatorios.
Es importante tener en cuenta que el modelo Theta puede ser demasiado simple para capturar patrones más complejos presentes en muchas series de tiempo. En tales casos, modelos más sofisticados como ARIMA (Autoregressive Integrated Moving Average) o modelos de suavizado exponencial pueden ser más apropiados.
El modelo Theta puede ser también paramétric y tiene la siguiente forma general dada por
Donde:
. Al introducir los parámetros θ0,θ1,...,θp en el modelo Theta, se permite una mayor flexibilidad para capturar diferentes patrones y tendencias en la serie de tiempo. Estos parámetros se estiman a través de métodos estadísticos, como la estimación de máxima verosimilitud, y su elección adecuada es fundamental para obtener pronósticos precisos.
Vamos con los códigos
from darts.models import ExponentialSmoothing, TBATS, AutoARIMA, Theta
def eval_model(model):
model.fit(train)
forecast = model.predict(len(val))
print("El modelo {} obtiene MAPE: {:.2f}%".format(model, mape(val, forecast)))
eval_model(ExponentialSmoothing())
eval_model(TBATS())
eval_model(AutoARIMA())
eval_model(Theta())
El modelo ExponentialSmoothing() obtiene MAPE: 5.11% El modelo TBATS() obtiene MAPE: 5.87% El modelo AutoARIMA() obtiene MAPE: 11.65% El modelo Theta() obtiene MAPE: 8.15%
Aquí, solo construimos estos modelos con sus parámetros predeterminados. Probablemente podamos hacerlo mejor si nos ajustamos a nuestro problema. Probemos con el método Theta.
El modelo Theta
contiene una implementación del método Theta de Assimakopoulos y Nikolopoulos. Este método ha tenido cierto éxito, particularmente en la competencia M3.
Aunque el valor del parámetro Theta a menudo se establece en 0 en las aplicaciones, la implementación Darts
admite un valor variable para fines de ajuste de parámetros. Tratemos de encontrar un buen valor para Theta:
# Buscar el mejor parámetro theta probando 50 valores diferentes
thetas = 2 - np.linspace(-10, 10, 50)
best_mape = float("inf")
best_theta = 0
for theta in thetas:
model = Theta(theta)
model.fit(train)
pred_theta = model.predict(len(val))
res = mape(val, pred_theta)
if res < best_mape:
best_mape = res
best_theta = theta
best_theta_model = Theta(best_theta)
best_theta_model.fit(train)
pred_best_theta = best_theta_model.predict(len(val))
print(
"El MAPE es: {:.2f}, con theta = {}.".format(
mape(val, pred_best_theta), best_theta
)
)
El MAPE es: 4.40, con theta = -3.5102040816326543.
train.plot(label="entrenamiento")
val.plot(label="verdadero")
pred_best_theta.plot(label="predicción")
We can observe that the model with best_theta
is so far the best we have, in terms of MAPE.
Entonces, en este punto, tenemos un modelo que funciona bien en nuestro conjunto de validación, y eso es bueno. Pero, ¿cómo podemos saber el rendimiento que habríamos obtenido si hubiéramos estado usando este modelo históricamente?
El backtesting simula predicciones que históricamente se habrían obtenido con un modelo dado. La producción puede tardar un tiempo, ya que el modelo se vuelve a entrenar (de forma predeterminada) cada vez que avanza el tiempo de predicción simulado.
Dichos pronósticos simulados siempre se definen con respecto a un horizonte de pronóstico, que es el número de pasos de tiempo que separan el tiempo de predicción del tiempo de pronóstico. En el siguiente ejemplo, simulamos pronósticos hechos para 3 meses en el futuro (en comparación con el tiempo de predicción). El resultado de llamar a historical_forecasts()
es (por defecto) una TimeSeries
que contiene esos pronósticos de 3 meses por delante:
historical_fcast_theta = best_theta_model.historical_forecasts(
series, start=0.6, forecast_horizon=3, verbose=True
)
0%| | 0/57 [00:00<?, ?it/s]
series.plot(label="datos")
historical_fcast_theta.plot(label="pronóstico backtest 3-meses adelante (Theta)")
print("MAPE = {:.2f}%".format(mape(historical_fcast_theta, series)))
MAPE = 7.70%
Entonces, parece que nuestro mejor modelo en el conjunto de validación ya no funciona tan bien cuando lo probamos (¿escuché sobreajuste: D?)
Para observar más de cerca los errores, también podemos usar el método backtest()
para obtener todos los errores sin procesar (digamos, errores MAPE) que habría obtenido nuestro modelo:
best_theta_model = Theta(best_theta)
raw_errors = best_theta_model.backtest(
series, start=0.6, forecast_horizon=3, metric=mape, reduction=None, verbose=True
)
from darts.utils.statistics import plot_hist
plot_hist(
raw_errors,
bins=np.arange(0, max(raw_errors), 1),
title="Puntuaciones individuales de errores de backtest (histograma)",
)
0%| | 0/57 [00:00<?, ?it/s]
Finalmente, usando backtest()
también podemos obtener una vista más simple del error promedio sobre los pronósticos históricos:
average_error = best_theta_model.backtest(
series,
start=0.6,
forecast_horizon=3,
metric=mape,
reduction=np.mean, # this is actually the default
verbose=True,
)
print("Error promedio (MAPE) sobre todos los pronósticos históricos: %.2f" % average_error)
0%| | 0/57 [00:00<?, ?it/s]
Error promedio (MAPE) sobre todos los pronósticos históricos: 6.36
We could also for instance have specified the argument reduction=np.median
to get the median MAPE instead.
Veamos los valores residuales ajustados de nuestro modelo Theta actual, es decir, la diferencia entre los pronósticos de 1 paso en cada punto en el tiempo obtenidos ajustando el modelo en todos los puntos anteriores y los valores reales observados:
from darts.utils.statistics import plot_residuals_analysis
plot_residuals_analysis(best_theta_model.residuals(series))
`start` time `1949-04-01 00:00:00` is before the first predictable/trainable historical forecasting point for series at index: 0. Ignoring `start` for this series and beginning at first trainable/predictable time: 1949-05-01 00:00:00. To hide these warnings, set `show_warnings=False`.
Podemos ver que la distribución no está centrada en 0, lo que significa que nuestro modelo Theta
está sesgado. También podemos distinguir un valor ACF grande con un desfase igual a 12, lo que indica que los residuos contienen información que no fue utilizada por el modelo.
¿Podríamos tal vez hacerlo mejor con un simple modelo ExponentialSmoothing
?
model_es = ExponentialSmoothing(seasonal_periods=12)
historical_fcast_es = model_es.historical_forecasts(
series, start=0.6, forecast_horizon=3, verbose=True
)
series.plot(label="datos")
historical_fcast_es.plot(label="pronóstico backtest 3 meses (suavizamiento exp.)")
print("MAPE = {:.2f}%".format(mape(historical_fcast_es, series)))
0%| | 0/57 [00:00<?, ?it/s]
MAPE = 4.45%
¡Así mucho mejor! En este caso, obtenemos un error porcentual absoluto medio de alrededor del 4-5% cuando realizamos una prueba retrospectiva con un horizonte de pronóstico de 3 meses.
plot_residuals_analysis(model_es.residuals(series))
El análisis de residuales también refleja un mejor desempeño en el sentido de que ahora tenemos una distribución de los residuales centrada en el valor 0, y los valores de ACF, aunque no son insignificantes, tienen magnitudes más bajas.
Darts
tiene un rico soporte para modelos de pronóstico de aprendizaje automático y aprendizaje profundo; por ejemplo:
Además de admitir la misma interfaz básica fit()
/predict()
que los otros modelos, estos modelos también son modelos globales, ya que admiten el entrenamiento en varias series temporales (a veces denominados metaaprendizaje). ).
Este es un punto clave del uso de modelos basados en ML para la previsión: la mayoría de las veces, los modelos de ML (especialmente los modelos de aprendizaje profundo) deben entrenarse con grandes cantidades de datos, lo que a menudo significa una gran cantidad de series temporales separadas pero relacionadas.
En Darts
, la forma básica de especificar múltiples 'TimeSeries' es usando una 'Secuencia' de 'TimeSeries' (por ejemplo, una lista simple de 'TimeSeries').
Estos modelos se pueden entrenar en miles de series. Aquí, por el bien de la ilustración, cargaremos dos series distintas: el conteo de pasajeros del tráfico aéreo y otra serie que contiene la cantidad de libras de leche producidas por vaca mensualmente. También colocamos nuestra serie en formato np.float32
ya que eso acelerará ligeramente el entrenamiento:
from darts.datasets import AirPassengersDataset, MonthlyMilkDataset
series_air = AirPassengersDataset().load().astype(np.float32)
series_milk = MonthlyMilkDataset().load().astype(np.float32)
# reservar los últimos 36 meses de cada serie como conjunto de validación:
train_air, val_air = series_air[:-36], series_air[-36:]
train_milk, val_milk = series_milk[:-36], series_milk[-36:]
train_air.plot()
val_air.plot()
train_milk.plot()
val_milk.plot()
Primero, escalemos estas dos series entre 0 y 1, ya que eso beneficiará a la mayoría de los modelos de ML. Usaremos un Scaler for this:
from darts.dataprocessing.transformers import Scaler
scaler = Scaler()
train_air_scaled, train_milk_scaled = scaler.fit_transform([train_air, train_milk])
train_air_scaled.plot()
train_milk_scaled.plot()
Observe cómo podemos escalar varias series de una sola vez. También podemos paralelizar este tipo de operaciones en múltiples procesadores especificandog n_jobs
.
N-BEATS
(Neural basis expansion analysis for interpretable time series forecasting) es un modelo de redes neuronales desarrollado para el análisis y pronóstico de series de tiempo. A diferencia de los enfoques tradicionales basados en ARIMA o modelos de suavizado exponencial, N-BEATS utiliza redes neuronales profundas para capturar patrones complejos y no lineales en los datos de la serie de tiempo. Para los detalles consulte el artículo N-BEATS: NEURAL BASIS EXPANSION ANALYSIS FOR INTERPRETABLE TIME SERIES FORECASTING.
El modelo N-BEATS está diseñado para ser interpretable y flexible en términos de la duración de las series de tiempo y las frecuencias de muestreo. Utiliza una arquitectura basada en bloques de transformadores (transformer blocks) que permiten capturar dependencias de largo alcance en los datos.
El concepto central del modelo N-BEATS es la expansión de base neuronal (neural basis expansion), que implica descomponer una serie de tiempo en una combinación de funciones de base que se aprenden durante el entrenamiento del modelo. Estas funciones de base capturan patrones y tendencias subyacentes en la serie de tiempo, lo que permite que el modelo realice pronósticos precisos.
La arquitectura de N-BEATS se compone de bloques de expansión (stacked fully connected layers) seguidos de bloques de pestañas (stacked gated linear units). Los bloques de expansión generan funciones de base y los bloques de pestañas combinan y ponderan estas funciones para generar las predicciones finales.
Una ventaja clave de N-BEATS es su interpretabilidad, ya que puede descomponer la serie de tiempo en funciones de base que se pueden analizar e interpretar individualmente. Esto proporciona información valiosa sobre los patrones y tendencias presentes en los datos.
En resumen, N-BEATS es un modelo de redes neuronales diseñado para el análisis y pronóstico de series de tiempo. Utiliza bloques de transformadores y la expansión de base neuronal para capturar patrones complejos en los datos. Su enfoque interpretable lo distingue de otros modelos de series de tiempo basados en redes neuronales.
A continuación, crearemos un modelo N-BEATS. Este modelo se puede ajustar con muchos hiperparámetros (como el número de pilas, capas, etc.). Aquí, por simplicidad, lo usaremos con hiperparámetros predeterminados. Los únicos dos hiperparámetros que tenemos que proporcionar son:
input_chunk_length
: esta es la "ventana retrospectiva" del modelo, es decir, cuántos pasos de tiempo de la historia toma la red neuronal como entrada para producir su salida en un paso hacia adelante.output_chunk_length
: esta es la "ventana de avance" del modelo, es decir, cuántos pasos de tiempo de valores futuros genera la red neuronal en un paso de avance.El parámetro random_state
solo está aquí para obtener resultados reproducibles.
La mayoría de las redes neuronales en Darts
requieren estos dos parámetros. Aquí, usaremos múltiplos de la estacionalidad. Ahora estamos listos para ajustar nuestro modelo en nuestras dos series (dando una lista que contiene las dos series para fit()
):
from darts.models import NBEATSModel
model = NBEATSModel(input_chunk_length=24, output_chunk_length=12, random_state=42)
model.fit([train_air_scaled, train_milk_scaled], epochs=50, verbose=True);
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs | Name | Type | Params --------------------------------------------------- 0 | criterion | MSELoss | 0 1 | train_metrics | MetricCollection | 0 2 | val_metrics | MetricCollection | 0 3 | stacks | ModuleList | 6.2 M --------------------------------------------------- 6.2 M Trainable params 1.4 K Non-trainable params 6.2 M Total params 24.787 Total estimated model params size (MB)
Training: 0it [00:00, ?it/s]
`Trainer.fit` stopped: `max_epochs=50` reached.
Veamos ahora algunos pronósticos a 36 meses para nuestras dos series. Podemos simplemente usar el argumento series
de la función fit()
para decirle al modelo qué serie pronosticar. Es importante destacar que output_chunk_length
no restringe directamente el horizonte de pronóstico n
que se puede usar con predict()
. Aquí, entrenamos el modelo con output_chunk_length=12
y generamos pronósticos para n=36
meses; esto simplemente se hace de forma autorregresiva detrás de escena (donde la red consume recursivamente sus salidas anteriores).
pred_air = model.predict(series=train_air_scaled, n=36)
pred_milk = model.predict(series=train_milk_scaled, n=36)
# scale back:
pred_air, pred_milk = scaler.inverse_transform([pred_air, pred_milk])
plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)")
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
¡Nuestros pronósticos en realidad no son tan terribles, considerando que usamos un modelo con hiperparámetros predeterminados para capturar tanto a los pasajeros aéreos como a la producción de leche!
El modelo parece bastante bueno para capturar la estacionalidad anual, pero pierde la tendencia de la serie aérea. En la siguiente sección, intentaremos resolver este problema utilizando datos externos (covariables).
Además de la serie objetivo (la serie que nos interesa pronosticar), muchos modelos en Darts también aceptan series de covariables en la entrada. Las covariables son series que no queremos pronosticar, pero que pueden proporcionar información adicional útil a los modelos. Tanto los objetivos como las covariables pueden ser multivariantes o univariantes.
Hay dos tipos de series temporales de covariables en Darts:
past_covariates
son series que no necesariamente se conocen antes del tiempo de pronóstico. Esos pueden, por ejemplo, representar cosas que deben medirse y no se conocen por adelantado. Los modelos no usan los valores futuros de past_covariates
cuando hacen pronósticos.future_covariates
son series que se conocen de antemano, hasta el horizonte de pronóstico. Esto puede representar cosas como información de calendario, días festivos, pronósticos del tiempo, etc. Los modelos que aceptan future_covariates
observarán los valores futuros (hasta el horizonte de pronóstico) al hacer pronósticos.Fuente : Darts
Cada covariable puede ser potencialmente multivariante. Si tiene varias series de covariables (como valores de mes y año), debe 'apilar' stack() o 'concatenar' concatenate() para obtener una serie multivariante.
Las covariables que proporcione pueden ser más largas de lo necesario. Darts intentará ser inteligente y cortarlos de la manera correcta para pronosticar el objetivo, en función de los índices de tiempo de las diferentes series. Sin embargo, recibirá un error si sus covariables no tienen un período de tiempo suficiente.
Ahora construyamos algunas covariables externas que contengan valores mensuales y anuales para nuestra serie de aire y leche.
En la celda de abajo, usamos la función darts.utils.timeseries_generation.datetime_attribute_timeseries()
para generar series que contienen los valores de mes y año, y concatenamos
estas series a lo largo del eje "componente"
para obtener una serie de covariables con dos componentes (mes y año), por serie objetivo. Para simplificar, escalamos directamente los valores de mes y año para tenerlos entre (aproximadamente) 0 y 1:
from darts import concatenate
from darts.utils.timeseries_generation import datetime_attribute_timeseries as dt_attr
air_covs = concatenate(
[
dt_attr(series_air.time_index, "month", dtype=np.float32) / 12,
(dt_attr(series_air.time_index, "year", dtype=np.float32) - 1948) / 12,
],
axis="component",
)
milk_covs = concatenate(
[
dt_attr(series_milk.time_index, "month", dtype=np.float32) / 12,
(dt_attr(series_milk.time_index, "year", dtype=np.float32) - 1962) / 13,
],
axis="component",
)
air_covs.plot()
plt.title(
"Una serie de tiempo multivariante de 2 dimensiones, que contiene covariables para la serie de passengers:"
);
No todos los modelos admiten todos los tipos de covariables. NBEATSModel
solo admite past_covariates
. Por lo tanto, aunque nuestras covariables representen información de calendario y se conozcan de antemano, las usaremos como covariables_pasadas
con N-BEATS. Para entrenar, todo lo que tenemos que hacer es darles como past_covariates
a la función fit()
, en el mismo orden que los objetivos:
model = NBEATSModel(input_chunk_length=24, output_chunk_length=12, random_state=42)
model.fit(
[train_air_scaled, train_milk_scaled],
past_covariates=[air_covs, milk_covs],
epochs=50,
verbose=True,
);
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs | Name | Type | Params --------------------------------------------------- 0 | criterion | MSELoss | 0 1 | train_metrics | MetricCollection | 0 2 | val_metrics | MetricCollection | 0 3 | stacks | ModuleList | 6.6 M --------------------------------------------------- 6.6 M Trainable params 1.7 K Non-trainable params 6.6 M Total params 26.314 Total estimated model params size (MB)
Training: 0it [00:00, ?it/s]
`Trainer.fit` stopped: `max_epochs=50` reached.
Luego, para producir pronósticos, nuevamente tenemos que proporcionar nuestras covariables como covariables_pasadas
a la función predict()
. Aunque la serie de tiempo de las covariables también contiene valores "futuros" de las covariables hasta el horizonte de pronóstico, el modelo no consumirá esos valores futuros, porque los usa como covariables pasadas (y no covariables futuras).
pred_air = model.predict(series=train_air_scaled, past_covariates=air_covs, n=36)
pred_milk = model.predict(series=train_milk_scaled, past_covariates=milk_covs, n=36)
# regresa a la escala original
pred_air, pred_milk = scaler.inverse_transform([pred_air, pred_milk])
plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)")
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
Parece que ahora el modelo captura mejor la tendencia de la serie del aire (que también perturba un poco los pronósticos de la serie de la leche).
El uso de covariables relacionadas con el calendario o el eje del tiempo (como meses y años como en nuestro ejemplo anterior) es tan frecuente que los modelos de aprendizaje profundo en Darts tienen una funcionalidad integrada para usar tales covariables listas para usar.
Para integrar fácilmente tales covariables a su modelo, simplemente puede especificar el parámetro add_encoders
en la creación del modelo. Este parámetro debe ser un diccionario que contenga información sobre lo que debe codificarse como covariables adicionales. Aquí hay un ejemplo de cómo podría verse un diccionario de este tipo, para un modelo que admita covariables pasadas y futuras:
encoders = {
"cyclic": {"future": ["month"]},
"datetime_attribute": {"future": ["hour", "dayofweek"]},
"position": {"past": ["absolute"], "future": ["relative"]},
"custom": {"past": [lambda idx: (idx.year - 1950) / 50]},
"transformer": Scaler(),
}
En el diccionario anterior, se especifican las siguientes cosas:
Scaler
, que se ajustará al llamar a la función fit()
del modelo y se usará después para transformar las covariables.Lo remitimos a la documentación de la API para obtener más información sobre cómo usar codificadores.
Para replicar nuestro ejemplo con el mes y el año usados como covariables anteriores con N-BEATS, podemos usar algunos codificadores de la siguiente manera:
encoders = {"datetime_attribute": {"past": ["month", "year"]}, "transformer": Scaler()}
Ahora, todo el entrenamiento del modelo N-BEATS con estas covariables tiene el siguiente aspecto:
model = NBEATSModel(
input_chunk_length=24,
output_chunk_length=12,
add_encoders=encoders,
random_state=42,
)
model.fit([train_air_scaled, train_milk_scaled], epochs=50, verbose=True);
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs | Name | Type | Params --------------------------------------------------- 0 | criterion | MSELoss | 0 1 | train_metrics | MetricCollection | 0 2 | val_metrics | MetricCollection | 0 3 | stacks | ModuleList | 6.6 M --------------------------------------------------- 6.6 M Trainable params 1.7 K Non-trainable params 6.6 M Total params 26.314 Total estimated model params size (MB)
Training: 0it [00:00, ?it/s]
`Trainer.fit` stopped: `max_epochs=50` reached.
Y obtenga algunos pronósticos para la serie de pasajeros aéreos:
pred_air = model.predict(series=train_air_scaled, n=36)
# scale back:
pred_air = scaler.inverse_transform(pred_air)
plt.figure(figsize=(10, 6))
series_air.plot(label="real (air)")
pred_air.plot(label="pronóstic (air)")
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
RegressionModel's son modelos de pronóstico que envuelven modelos de regresión compatibles con sklearn. El modelo de regresión interna se utiliza para predecir valores futuros de la serie objetivo, en función de ciertos retrasos de las covariables objetivo, pasadas y futuras. Detrás de escena, las series de tiempo se tabularizan para construir un conjunto de datos de entrenamiento en el formato correcto. La imagen muestra como se tabulan los datos para implementar un modelo de pronóstico por regresión para el caso univariado.
Fuente : Alvaro Montenegro
Y la siguiente imagen ilustra como se preparan los datos para los pronósticos multivariados. Darts, hace todo el trabajo por nosotros.
Fuente : Alvaro Montenegro
Por defecto, RegressionModel
hará una regresión lineal. Es muy fácil usar cualquier modelo de regresión compatible con sklearn deseado especificando el parámetro 'modelo', pero para mayor comodidad, Darts también proporciona un par de modelos listos para usar:
sklearn.ensemble.RandomForestRegressor
.lightbm
.sklearn.linear_model.LinearRegression
acepta los mismos kwargs).Por ejemplo, así es como se ve el ajuste de una regresión rígido bayesiana a nuestro problema de dos series de juguetes:
from darts.models import RegressionModel
from sklearn.linear_model import BayesianRidge
model = RegressionModel(lags=72, lags_future_covariates=[-6, 0], model=BayesianRidge())
model.fit(
[train_air_scaled, train_milk_scaled], future_covariates=[air_covs, milk_covs]
);
Arriba pasaron varias cosas:
lags=72
le dice al RegressionModel
que mire los últimos 72 retrasos del objetivo.lags_future_covariates=[-6, 0]
significa que el modelo también observará los retrasos de las future_covariates
que proporcionamos. Aquí enumeramos los retrasos precisos que queremos que los modelos tengan en cuenta; los retrasos "-6th" y "0th". El retraso "0" significa el retraso "actual" (es decir, en el paso de tiempo que se pronostica); obviamente, conocer este retraso requiere conocer los datos de antemano (de ahí el hecho de que estemos usando future_covariates
). De manera similar, -6
significa que también observamos el valor de las covariables 6 meses antes del paso de tiempo pronosticado (que también requiere conocer las covariables por adelantado si estamos pronosticando en un horizonte de más de 6 pasos adelante).model=BayesianRidge()
proporciona el modelo de regresión interno real.Ahora vamos a obtener algunos pronósticos:
pred_air, pred_milk = model.predict(
series=[train_air_scaled, train_milk_scaled],
future_covariates=[air_covs, milk_covs],
n=36,
)
# scale back:
pred_air, pred_milk = scaler.inverse_transform([pred_air, pred_milk])
plt.figure(figsize=(10, 6))
series_air.plot(label="real (aire)")
series_milk.plot(label="real (leche)")
pred_air.plot(label="pronóstico (aire)")
pred_milk.plot(label="pronóstico (leche)")
Observe cómo obtuvimos los pronósticos para las dos series de tiempo a la vez arriba. Del mismo modo, también podemos obtener algunas métricas sobre secuencias de series:
mape([series_air, series_milk], [pred_air, pred_milk])
[3.417011722922325, 5.283196270465851]
o la métrica promedio sobre "todas" las series:
mape([series_air, series_milk], [pred_air, pred_milk], inter_reduction=np.mean)
4.350103996694088
Por cierto: de manera similar a los transformadores como Scaler
, las métricas informativas se pueden paralelizar en N
procesadores cuando se ejecutan en muchos pares de series especificando n_jobs=N
.
Parece que este modelo funciona bien en la serie de tráfico aéreo, ¿cómo funciona cuando lo probamos en esta serie?
bayes_ridge_model = RegressionModel(
lags=72, lags_future_covariates=[0], model=BayesianRidge()
)
backtest = bayes_ridge_model.historical_forecasts(
series_air, future_covariates=air_covs, start=0.6, forecast_horizon=3, verbose=True
)
print("MAPE = %.2f" % (mape(backtest, series_air)))
series_air.plot()
backtest.plot()
0%| | 0/57 [00:00<?, ?it/s]
MAPE = 3.66
¡Nuestro mejor modelo hasta ahora!
Algunos modelos pueden producir pronósticos probabilísticos. Este es el caso de todos los modelos de aprendizaje profundo (como RNNModel
, NBEATSModel
, etc...), así como ARIMA
y ExponentialSmoothing
. La lista completa está disponible en la página README de Darts.
Para ARIMA
y ExponentialSmoothing
, uno puede simplemente especificar un parámetro num_samples
a la función predict()
. La 'TimeSeries' devuelta estará compuesta por muestras de Monte Carlo 'num_samples' que describen la distribución de los valores de la serie temporal. La ventaja de confiar en las muestras de Monte Carlo (en contraste con, por ejemplo, los intervalos de confianza explícitos) es que se pueden usar para describir cualquier distribución conjunta paramétrica o no paramétrica sobre los componentes y calcular cuantiles arbitrarios.
model_es = ExponentialSmoothing()
model_es.fit(train)
probabilistic_forecast = model_es.predict(len(val), num_samples=500)
series.plot(label="real")
probabilistic_forecast.plot(label="pronóstico probabilístico")
plt.legend()
plt.show()
Con las redes neuronales, uno tiene que dar un objeto de "Verosimilitud" al modelo. Las probabilidades especifican a qué distribución intentará ajustarse el modelo, junto con posibles valores previos para los parámetros de las distribuciones. La lista completa de probabilidades disponibles está disponible en los documentos.
Usar probabilidades es fácil. Por ejemplo, así es como se ve el entrenamiento de un 'NBEATSModel' para que se ajuste a una probabilidad de Laplace:
from darts.models import TCNModel
from darts.utils.likelihood_models import LaplaceLikelihood
model = TCNModel(
input_chunk_length=24,
output_chunk_length=12,
random_state=42,
likelihood=LaplaceLikelihood(),
)
model.fit(train_air_scaled, epochs=400, verbose=True);
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs | Name | Type | Params ---------------------------------------------------- 0 | criterion | MSELoss | 0 1 | train_metrics | MetricCollection | 0 2 | val_metrics | MetricCollection | 0 3 | dropout | MonteCarloDropout | 0 4 | res_blocks | ModuleList | 166 ---------------------------------------------------- 166 Trainable params 0 Non-trainable params 166 Total params 0.001 Total estimated model params size (MB)
Training: 0it [00:00, ?it/s]
`Trainer.fit` stopped: `max_epochs=400` reached.
Luego, para obtener pronósticos probabilísticos, nuevamente solo necesitamos especificar algunos num_samples >> 1
:
pred = model.predict(n=36, num_samples=500)
# regresa a la escala original
pred = scaler.inverse_transform(pred)
series_air.plot()
pred.plot()
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
Además, también podríamos, por ejemplo, especificar que tenemos alguna creencia previa de que la escala de la distribución es de alrededor de 0.1 (en el dominio transformado), al mismo tiempo que capturamos alguna dependencia temporal de la distribución, especificando prior_b=.1
.
Detrás de escena, esto regularizará la pérdida de entrenamiento con un término de divergencia Kullback-Leibler.
model = TCNModel(
input_chunk_length=24,
output_chunk_length=12,
random_state=42,
likelihood=LaplaceLikelihood(prior_b=0.1),
)
model.fit(train_air_scaled, epochs=400, verbose=True);
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs | Name | Type | Params ---------------------------------------------------- 0 | criterion | MSELoss | 0 1 | train_metrics | MetricCollection | 0 2 | val_metrics | MetricCollection | 0 3 | dropout | MonteCarloDropout | 0 4 | res_blocks | ModuleList | 166 ---------------------------------------------------- 166 Trainable params 0 Non-trainable params 166 Total params 0.001 Total estimated model params size (MB)
Training: 0it [00:00, ?it/s]
`Trainer.fit` stopped: `max_epochs=400` reached.
pred = model.predict(n=36, num_samples=500)
# scale back:
pred = scaler.inverse_transform(pred)
series_air.plot()
pred.plot()
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
Por defecto, TimeSeries.plot()
muestra la mediana, así como los percentiles 5 y 95 (de las distribuciones marginales, si TimeSeries
es multivariante). Es posible controlar esto:
pred.plot(low_quantile=0.01, high_quantile=0.99, label="1-99th percentiles")
pred.plot(low_quantile=0.2, high_quantile=0.8, label="20-80th percentiles")
La probabilidad tiene que ser compatible con el dominio de los valores de su serie temporal. Por ejemplo
También es posible utilizar
¿Cómo podemos evaluar la calidad de los pronósticos probabilísticos? De forma predeterminada, la mayoría de las funciones de métricas (como mape()
) seguirán funcionando, pero solo observarán el pronóstico medio. También es posible utilizar la métrica de riesgo ρ (o pérdida cuantil), que cuantifica el error para cada cuantil previsto:
from darts.metrics import rho_risk
print("MAPE of median forecast: %.2f" % mape(series_air, pred))
for rho in [0.05, 0.1, 0.5, 0.9, 0.95]:
rr = rho_risk(series_air, pred, rho=rho)
print("rho-risk at quantile %.2f: %.2f" % (rho, rr))
MAPE of median forecast: 11.16 rho-risk at quantile 0.05: 0.13 rho-risk at quantile 0.10: 0.14 rho-risk at quantile 0.50: 0.11 rho-risk at quantile 0.90: 0.03 rho-risk at quantile 0.95: 0.02
¿Podríamos hacerlo mejor ajustando estos cuantiles directamente? Podemos simplemente usar una probabilidad QuantileRegression
:
from darts.utils.likelihood_models import QuantileRegression
model = TCNModel(
input_chunk_length=24,
output_chunk_length=12,
random_state=42,
likelihood=QuantileRegression([0.05, 0.1, 0.5, 0.9, 0.95]),
)
model.fit(train_air_scaled, epochs=400, verbose=True);
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs | Name | Type | Params ---------------------------------------------------- 0 | criterion | MSELoss | 0 1 | train_metrics | MetricCollection | 0 2 | val_metrics | MetricCollection | 0 3 | dropout | MonteCarloDropout | 0 4 | res_blocks | ModuleList | 208 ---------------------------------------------------- 208 Trainable params 0 Non-trainable params 208 Total params 0.001 Total estimated model params size (MB)
Training: 0it [00:00, ?it/s]
`Trainer.fit` stopped: `max_epochs=400` reached.
pred = model.predict(n=36, num_samples=500)
# scale back:
pred = scaler.inverse_transform(pred)
series_air.plot()
pred.plot()
print("MAPE del pronóstico de la mediana: %.2f" % mape(series_air, pred))
for rho in [0.05, 0.1, 0.5, 0.9, 0.95]:
rr = rho_risk(series_air, pred, rho=rho)
print("rho-risk at quantile %.2f: %.2f" % (rho, rr))
GPU available: False, used: False TPU available: False, using: 0 TPU cores IPU available: False, using: 0 IPUs HPU available: False, using: 0 HPUs
Predicting: 0it [00:00, ?it/s]
MAPE del pronóstico de la mediana: 7.78 rho-risk at quantile 0.05: 0.07 rho-risk at quantile 0.10: 0.08 rho-risk at quantile 0.50: 0.07 rho-risk at quantile 0.90: 0.02 rho-risk at quantile 0.95: 0.01
Ensambalado se trata de combinar los pronósticos producidos por varios modelos, para obtener un pronóstico final y, con suerte, mejor.
Por ejemplo, en nuestro ejemplo de un modelo menos ingenuo anterior, combinamos manualmente un modelo estacional ingenuo con un modelo de tendencia ingenuo. Aquí, mostraremos cómo los pronósticos de los modelos se pueden combinar automáticamente, de manera ingenua usando un NaiveEnsembleModel
, o aprendiendo usando RegressionEnsembleModel
.
Por supuesto, también es posible usar pasado
y/o futuras_covariantes
con el modelo de conjunto, pero solo se pasarán a los modelos que los soportan cuando se llame a fit()
y predict()
.
El ensamblaje ingenuo solo toma el promedio de los pronósticos de varios modelos. Darts proporciona un NaiveEnsembleModel
, que permite hacer esto mientras se manipula solo un modelo de pronóstico (que, por ejemplo, permite una prueba retrospectiva más sencilla):
from darts.models import NaiveEnsembleModel
models = [NaiveDrift(), NaiveSeasonal(12)]
ensemble_model = NaiveEnsembleModel(models=models)
backtest = ensemble_model.historical_forecasts(
series_air, start=0.6, forecast_horizon=3, verbose=True
)
print("MAPE = %.2f" % (mape(backtest, series_air)))
series_air.plot()
backtest.plot()
0%| | 0/57 [00:00<?, ?it/s]
MAPE = 11.88
Como era de esperar en este caso, el conjunto ingenuo no da grandes resultados (¡aunque en algunos casos podría!)
A veces podemos hacerlo mejor si vemos el conjunto como un problema de regresión supervisado: dado un conjunto de pronósticos (características), encuentre un modelo que los combine para minimizar los errores en el objetivo.
Esto es lo que hace RegressionEnsembleModel
. Acepta tres parámetros:
forecasting_models
es una lista de modelos de pronóstico cuyas predicciones queremos ensamblar.regression_train_n_points
es el número de pasos de tiempo a usar para ajustar el modelo de "regresión de conjunto" (es decir, el modelo interno que combina los pronósticos).regression_model
es, opcionalmente, un modelo de regresión compatible con sklearn o un RegressionModel
de Darts que se utilizará para la regresión de conjunto. Si no se especifica, se utiliza una regresión lineal. Usar un modelo sklearn es fácil de usar, pero usar un RegressionModel
permite potencialmente tomar retrasos arbitrarios de los pronósticos individuales como entradas del modelo de regresión.Una vez que estos elementos están en su lugar, se puede usar un RegressionEnsembleModel
como un modelo de pronóstico regular:
from darts.models import RegressionEnsembleModel
models = [NaiveDrift(), NaiveSeasonal(12)]
ensemble_model = RegressionEnsembleModel(
forecasting_models=models, regression_train_n_points=12
)
backtest = ensemble_model.historical_forecasts(
series_air, start=0.6, forecast_horizon=3, verbose=True
)
print("MAPE = %.2f" % (mape(backtest, series_air)))
series_air.plot()
backtest.plot()
0%| | 0/57 [00:00<?, ?it/s]
MAPE = 4.85
We can also inspect the coefficients used to weigh the two inner models in the linear combination:
ensemble_model.fit(series_air)
ensemble_model.regression_model.model.coef_
array([0.01368787, 1.0980107 ], dtype=float32)
Además de los modelos de pronóstico, que son capaces de predecir valores futuros de series, Darts también contiene un par de modelos útiles de filtrado, que pueden modelar distribuciones de valores de series "en muestra".
KalmanFilter
implementa un Kalman Filter. La implementación se basa en nfoursid, por lo que es posible, por ejemplo, proporcionar un objeto nfoursid.kalman.Kalman
que contenga una matriz de transición , covarianza de ruido de proceso, covarianza de ruido de observación, etc.
También es posible realizar la identificación del sistema llamando a fit()
para "entrenar" el filtro Kalman utilizando el algoritmo de identificación del sistema N4SID:
from darts.models import KalmanFilter
kf = KalmanFilter(dim_x=3)
kf.fit(train_air_scaled)
filtered_series = kf.filter(train_air_scaled, num_samples=100)
train_air_scaled.plot()
filtered_series.plot()
Darts también contiene un GaussianProcessFilter
que se puede usar para el modelado probabilístico de series:
from darts.models import GaussianProcessFilter
from sklearn.gaussian_process.kernels import RBF
# create a series with holes:
values = train_air_scaled.values()
values[20:22] = np.nan
values[28:32] = np.nan
values[55:59] = np.nan
values[72:80] = np.nan
series_holes = TimeSeries.from_times_and_values(train_air_scaled.time_index, values)
series_holes.plot()
kernel = RBF()
gpf = GaussianProcessFilter(kernel=kernel, alpha=0.1, normalize_y=True)
filtered_series = gpf.filter(series_holes, num_samples=100)
filtered_series.plot()
Entonces, ¿N-BEATS, el suavizado exponencial o una regresión de cresta bayesiana entrenada en la producción de leche es el mejor enfoque para predecir el número futuro de pasajeros de aerolíneas? Bueno, en este punto es realmente difícil decir exactamente cuál es el mejor. Nuestra serie temporal es pequeña y nuestro conjunto de validación es aún más pequeño. En tales casos, es muy fácil adaptar todo el ejercicio de pronóstico a un conjunto de validación tan pequeño. Eso es especialmente cierto si la cantidad de modelos disponibles y sus grados de libertad es alta (como para los modelos de aprendizaje profundo) o si jugamos con muchos modelos en un solo conjunto de prueba (como se hizo en este portátil).
Como científicos de datos, es nuestra responsabilidad comprender hasta qué punto se puede confiar en nuestros modelos. Así que siempre tome los resultados con pinzas, especialmente en conjuntos de datos pequeños, y aplique el método científico antes de hacer cualquier tipo de pronóstico :) ¡Feliz modelado!