Más sobre forecasting en cienciadedatos.net
- Forecasting series temporales con machine learning
- Modelos ARIMA y SARIMAX
- Forecasting series temporales con gradient boosting: XGBoost, LightGBM y CatBoost
- Global Forecasting: Multi-series forecasting
- Forecasting de la demanda eléctrica con machine learning
- Forecasting con deep learning
- Forecasting de visitas a página web con machine learning
- Forecasting del precio de Bitcoin
- Forecasting probabilístico
- Forecasting de demanda intermitente
- Reducir el impacto del Covid en modelos de forecasting
- Modelar series temporales con tendencia utilizando modelos de árboles

Introducción¶
Deep Learning es un campo de la inteligencia artificial centrado en crear modelos basados en redes neuronales que permiten aprender representaciones no lineales. Las redes neuronales recurrentes (RNN) son un tipo de arquitectura de deep learning diseñada para trabajar con datos secuenciales, donde la información se propaga a través de conexiones recurrentes, lo que permite a la red aprender patrones temporales.
Este artículo describe cómo entrenar modelos de redes neuronales recurrentes, específicamente RNN, GRU y LSTM, para la predicción de series temporales (forecasting) utilizando Python, Keras y skforecast.
Keras3 proporciona una interfaz amigable para construir y entrenar modelos de redes neuronales. Gracias a su API de alto nivel, los desarrolladores pueden implementar fácilmente arquitecturas LSTM, aprovechando la eficiencia computacional y la escalabilidad que ofrece el deep learning.
Skforecast facilita la implementación y uso de modelos de machine learning para problemas de predicción. Con este paquete, el usuario puede definir el problema y abstraerse de la arquitectura. Para usuarios avanzados, skforecast también permite ejecutar una arquitectura de deep learning previamente definida.
✎ Nota
Para comprender plenamente este artículo, se presupone cierto conocimiento sobre redes neuronales y deep learning. No obstante, si este no es el caso, y mientras trabajamos en la creación de nuevo material, te proporcionamos algunos enlaces de referencia para comenzar:
Redes Neuronales Recurrentes (RNN)¶
Las Redes Neuronales Recurrentes (RNN) son una familia de modelos específicamente diseñados para trabajar con datos secuenciales, como las series temporales. A diferencia de las redes neuronales tradicionales (feedforward), que tratan cada entrada de forma independiente, las RNN incorporan una memoria interna que les permite capturar dependencias entre los elementos de una secuencia. Esto permite al modelo aprovechar la información de los pasos previos para mejorar las predicciones futuras.
El bloque fundamental de una RNN es la célula recurrente, que en cada paso temporal recibe dos entradas: el dato actual y el estado oculto anterior (la "memoria" de la red). En cada iteración, el estado oculto se actualiza almacenando la información relevante de la secuencia hasta ese momento. Esta arquitectura permite que las RNN puedan “recordar” tendencias y patrones a lo largo del tiempo.
Sin embargo, las RNN simples tienen dificultades para aprender dependencias a largo plazo debido a problemas como el desvanecimiento o explosión del gradiente. Para superar estas limitaciones, se desarrollaron arquitecturas más avanzadas como las Long Short-Term Memory (LSTM) y las Gated Recurrent Unit (GRU). Estas variantes son más eficaces capturando patrones complejos y de largo alcance en datos de series temporales.
Diagrama de una red RNN simple. Fuente: James, G., Witten, D., Hastie, T., & Tibshirani, R. (2013). An introduction to statistical learning (1st ed.) [PDF]. Springer.
Long Short-Term Memory (LSTM)¶
Las Long Short-Term Memory (LSTM) son un tipo de red neuronal recurrente ampliamente utilizada, diseñada para capturar de forma efectiva dependencias a largo plazo en datos secuenciales. A diferencia de las RNN simples, las LSTM emplean una arquitectura más sofisticada basada en un sistema de celdas de memoria y compuertas (gates) que controlan el flujo de información a lo largo del tiempo.
El componente central de una LSTM es la celda de memoria, que mantiene la información a través de los distintos pasos temporales. Tres compuertas regulan cómo se añade, retiene o descarta la información en cada iteración:
Forget Gate (Compuerta de Olvido): Decide qué información del estado previo de la celda debe eliminarse. Utiliza la entrada actual y el estado oculto anterior, aplicando una activación sigmoide para obtener un valor entre 0 y 1 (donde 0 significa “olvidar completamente” y 1 significa “conservar completamente”).
Input Gate (Compuerta de Entrada): Controla cuánta información nueva se añade al estado de la celda, también usando la entrada actual y el estado oculto previo con una activación sigmoide.
Output Gate (Compuerta de Salida): Determina cuánta información del estado de la celda se muestra como salida y se transmite al siguiente estado oculto.
Este mecanismo de compuertas permite a las LSTM recordar o “olvidar” información de manera selectiva, lo que las hace especialmente eficaces para modelar secuencias con patrones de largo plazo.
Diagrama de entradas y salidas de una LSTM. Fuente: codificandobits https://databasecamp.de/wp-content/uploads/lstm-architecture-1024x709.png.
Las Gated Recurrent Unit (GRU) son una alternativa simplificada a las LSTM, ya que utilizan solo dos compuertas (reset y update), pero suelen alcanzar un rendimiento similar. Las GRU requieren menos parámetros y pueden ser computacionalmente más eficientes, lo que puede suponer una ventaja en ciertas tareas o cuando se trabaja con conjuntos de datos de gran tamaño.
Tipos de capas recurrentes en skforecast¶
Con skforecast, puedes utilizar tres tipos de células recurrentes:
Simple RNN: Adecuada para problemas con dependencias de corto plazo o cuando un modelo sencillo es suficiente. Es menos eficaz capturando patrones a largo plazo.
LSTM (Long Short-Term Memory): Incorpora mecanismos de compuertas que permiten a la red aprender y retener información durante periodos más largos. Las LSTM son una opción popular para muchos problemas de predicción complejos.
GRU (Gated Recurrent Unit): Ofrece una estructura más simple que la LSTM, utilizando menos parámetros y logrando un rendimiento comparable en muchos escenarios. Resulta útil cuando la eficiencia computacional es importante.
✎ Note
Recomendaciones para elegir una capa recurrente:- Utiliza LSTM si tu serie temporal presenta patrones a largo plazo o dependencias complejas.
- Prueba con GRU como alternativa más ligera a LSTM.
- Emplea Simple RNN solo en tareas sencillas o como modelo de referencia.
Tipos de problemas en forecasting¶
La complejidad de un problema de predicción en series temporales suele estar determinada por tres preguntas clave:
¿Qué series se van a utilizar para entrenar el modelo?
¿Qué series (y cuántas) se quieren predecir?
¿Cuántos pasos hacia el futuro se desean predecir?
Estas decisiones condicionan la estructura de tu conjunto de datos y el diseño del modelo de predicción, y son esenciales a la hora de abordar problemas de series temporales.
Los modelos de deep learning para series temporales pueden abordar una gran variedad de escenarios de predicción, todo depende de cómo se estructuren los datos de entrada y qué objetivos de predicción se definan. Estos modelos pueden modelar los siguientes escenarios:
Problemas 1:1 — Serie única, salida única
- Descripción: El modelo utiliza únicamente los valores pasados de una serie para predecir sus propios valores futuros. Es el enfoque clásico autorregresivo.
- Ejemplo: Predecir la temperatura de mañana usando solo las mediciones previas de temperatura.
Problemas N:1 — Multiserie, salida única
- Descripción: El modelo utiliza varias series como predictores, pero el objetivo es solo una de ellas. Cada predictor puede ser una variable o entidad diferente, pero solo se pronostica una serie de salida.
- Ejemplo: Predecir la temperatura de mañana usando la temperatura, humedad y presión atmosférica.
Problemas N:M — Multiserie, múltiples salidas
- Descripción: El modelo emplea múltiples series como predictores y pronostica varias series objetivo al mismo tiempo.
- Ejemplo: Pronosticar los valores en bolsa de varias acciones en función del histórico de la bolsa, del precio de la energía y materias primas.
Todos estos escenarios pueden plantearse tanto como single-step forecasting (predecir solo el siguiente punto temporal) o como multi-step forecasting (predecir varios puntos futuros).
Además, puedes mejorar tus modelos incorporando variables exógenas, es decir, características externas o información adicional conocida de antemano (como efectos de calendario, promociones o previsión meteorológica), junto con los datos principales de la serie temporal.
Definir la arquitectura de deep learning adecuada para cada caso puede ser todo un reto. La librería skforecast ayuda seleccionando automáticamente la arquitectura idónea en cada escenario, lo que facilita y acelera el proceso de modelado.
A continuación, encontrarás ejemplos de cómo usar skforecast para resolver cada uno de estos problemas de series temporales utilizando redes neuronales recurrentes.
Datos¶
Los datos empleados en este artículo contienen información detallada sobre la calidad del aire en la ciudad de Valencia (España). La colección de datos abarca desde el 1 de enero de 2019 hasta el 31 de diciembre de 2021, proporcionando mediciones horarias de diversos contaminantes atmosféricos, como partículas PM2.5 y PM10, monóxido de carbono (CO), dióxido de nitrógeno (NO2), entre otros. Los datos se han obtenido de plataforma Red de Vigilancia y Control de la Contaminación Atmosférica, 46250054-València - Centre, https://mediambient.gva.es/es/web/calidad-ambiental/datos-historicos.
Librerías¶
⚠ Warning
skforecast es compatible con varios backends de Keras: TensorFlow, JAX y PyTorch (torch). Puedes seleccionar el backend utilizando la variable de entornoKERAS_BACKEND
o editando tu archivo de configuración local en ~/.keras/keras.json
.
```python
import os
os.environ["KERAS_BACKEND"] = "tensorflow" # Opciones: "tensorflow", "jax", o "torch"
import keras
```
El backend debe configurarse antes de importar Keras en tu sesión de Python. Una vez que Keras ha sido importado, el backend no puede cambiarse sin reiniciar el entorno.
Como alternativa, puedes definir el backend en el archivo de configuración en ~/.keras/keras.json
:
```json
{
"backend": "tensorflow" # Opciones: "tensorflow", "jax", o "torch"
}
```
# Procesado de datos
# ==============================================================================
import os
import numpy as np
import pandas as pd
# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
import plotly.offline as poff
pio.templates.default = "seaborn"
poff.init_notebook_mode(connected=True)
# Keras
# ==============================================================================
os.environ["KERAS_BACKEND"] = "tensorflow" # 'tensorflow', 'jax´ or 'torch'
import keras
from keras.optimizers import Adam
from keras.losses import MeanSquaredError
from keras.callbacks import EarlyStopping, ReduceLROnPlateau
# Feature engineering
# ==============================================================================
from sklearn.preprocessing import MinMaxScaler
from sklearn.pipeline import make_pipeline
from feature_engine.datetime import DatetimeFeatures
from feature_engine.creation import CyclicalFeatures
# Modelado
# ==============================================================================
import skforecast
from skforecast.plot import set_dark_theme
from skforecast.datasets import fetch_dataset
from skforecast.deep_learning import ForecasterRnn
from skforecast.deep_learning import create_and_compile_model
from skforecast.model_selection import TimeSeriesFold
from skforecast.model_selection import backtesting_forecaster_multiseries
from skforecast.plot import plot_prediction_intervals
# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings('once')
warnings.filterwarnings('ignore', category=DeprecationWarning, module='tensorflow.python.framework.ops')
color = '\033[1m\033[38;5;208m'
print(f"{color}skforecast version: {skforecast.__version__}")
print(f"{color}Keras version: {keras.__version__}")
print(f"{color}Using backend: {keras.backend.backend()}")
if keras.backend.backend() == "tensorflow":
import tensorflow
print(f"{color}tensorflow version: {tensorflow.__version__}")
elif keras.backend.backend() == "torch":
import torch
print(f"{color}torch version: {torch.__version__}")
else:
print(f"{color}Backend not recognized. Please use 'tensorflow' or 'torch'.")
skforecast version: 0.17.0 Keras version: 3.10.0 Using backend: tensorflow tensorflow version: 2.19.0
# Descarga y procesado de datos
# ==============================================================================
data = fetch_dataset(name="air_quality_valencia_no_missing")
data.head()
air_quality_valencia_no_missing ------------------------------- Hourly measures of several air chemical pollutant at Valencia city (Avd. Francia) from 2019-01-01 to 20213-12-31. Including the following variables: pm2.5 (µg/m³), CO (mg/m³), NO (µg/m³), NO2 (µg/m³), PM10 (µg/m³), NOx (µg/m³), O3 (µg/m³), Veloc. (m/s), Direc. (degrees), SO2 (µg/m³). Missing values have been imputed using linear interpolation. Red de Vigilancia y Control de la Contaminación Atmosférica, 46250047-València - Av. França, https://mediambient.gva.es/es/web/calidad-ambiental/datos- historicos. Shape of the dataset: (43824, 10)
so2 | co | no | no2 | pm10 | nox | o3 | veloc. | direc. | pm2.5 | |
---|---|---|---|---|---|---|---|---|---|---|
datetime | ||||||||||
2019-01-01 00:00:00 | 8.0 | 0.2 | 3.0 | 36.0 | 22.0 | 40.0 | 16.0 | 0.5 | 262.0 | 19.0 |
2019-01-01 01:00:00 | 8.0 | 0.1 | 2.0 | 40.0 | 32.0 | 44.0 | 6.0 | 0.6 | 248.0 | 26.0 |
2019-01-01 02:00:00 | 8.0 | 0.1 | 11.0 | 42.0 | 36.0 | 58.0 | 3.0 | 0.3 | 224.0 | 31.0 |
2019-01-01 03:00:00 | 10.0 | 0.1 | 15.0 | 41.0 | 35.0 | 63.0 | 3.0 | 0.2 | 220.0 | 30.0 |
2019-01-01 04:00:00 | 11.0 | 0.1 | 16.0 | 39.0 | 36.0 | 63.0 | 3.0 | 0.4 | 221.0 | 30.0 |
Se verifica que el conjunto de datos tiene un índice de tipo DatetimeIndex
con frecuencia horaria.
# Comprobación de índice y frecuencia
# ==============================================================================
print(f"Tipo de índice : {data.index.dtype}")
print(f"Frecuencia : {data.index.freq}")
Tipo de índice : datetime64[ns] Frecuencia : <Hour>
Para facilitar el entrenamiento de los modelos y la evaluación de su capacidad predictiva, los datos se dividen en tres conjuntos separados: entrenamiento, validación y test.
# Split train-validation-test
# ==============================================================================
data = data.loc["2019-01-01 00:00:00":"2021-12-31 23:59:59", :].copy()
end_train = "2021-03-31 23:59:00"
end_validation = "2021-09-30 23:59:00"
data_train = data.loc[:end_train, :].copy()
data_val = data.loc[end_train:end_validation, :].copy()
data_test = data.loc[end_validation:, :].copy()
print(
f"Fechas train : {data_train.index.min()} --- "
f"{data_train.index.max()} (n={len(data_train)})"
)
print(
f"Fechas validation : {data_val.index.min()} --- "
f"{data_val.index.max()} (n={len(data_val)})"
)
print(
f"Fechas test : {data_test.index.min()} --- "
f"{data_test.index.max()} (n={len(data_test)})"
)
Fechas train : 2019-01-01 00:00:00 --- 2021-03-31 23:00:00 (n=19704) Fechas validation : 2021-04-01 00:00:00 --- 2021-09-30 23:00:00 (n=4392) Fechas test : 2021-10-01 00:00:00 --- 2021-12-31 23:00:00 (n=2208)
# Plot series
# ==============================================================================
set_dark_theme()
colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] * 2
fig, axes = plt.subplots(len(data.columns), 1, figsize=(8, 8), sharex=True)
for i, col in enumerate(data.columns):
axes[i].plot(data[col], label=col, color=colors[i])
axes[i].legend(loc='upper right', fontsize=8)
axes[i].tick_params(axis='both', labelsize=8)
axes[i].axvline(pd.to_datetime(end_train), color='white', linestyle='--', linewidth=1) # End train
axes[i].axvline(pd.to_datetime(end_validation), color='white', linestyle='--', linewidth=1) # End validation
fig.suptitle("Air Quality Valencia", fontsize=16)
plt.tight_layout()
Creación sencilla de modelos basados en RNN con create_and_compile_model
¶
skforecast proporciona la función create_and_compile_model
para simplificar la creación de arquitecturas de redes neuronales recurrentes (RNN, LSTM o GRU) para la predicción de series temporales. Esta función está diseñada para facilitar tanto a usuarios principiantes como avanzados la construcción y compilación de modelos de Keras con solo unas pocas líneas de código.
Uso básico
Para la mayoría de los escenarios de predicción, es suficiente con especificar los datos de la serie temporal, el número de observaciones rezagadas (lags), el número de pasos a predecir y el tipo de capa recurrente que deseas utilizar (LSTM, GRU o SimpleRNN). Por defecto, la función establece parámetros razonables para cada capa, aunque todos los parámetros de la arquitectura pueden ajustarse según las necesidades específicas.
# Uso básico de `create_and_compile_model`
# ==============================================================================
model = create_and_compile_model(
series = data, # Las 10 series se usan como predictores
levels = ["o3"], # Serie a predecir
lags = 32, # Número de lags a usar como predictores
steps = 24, # Número de steps a predecir
recurrent_layer = "LSTM", # Tipo de capa recurrente ('LSTM', 'GRU', or 'RNN')
recurrent_units = 100, # Número de unidades en la capa recurrente
dense_units = 64 # Número de unidades en la capa densa
)
model.summary()
keras version: 3.10.0 Using backend: tensorflow tensorflow version: 2.19.0
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ series_input (InputLayer) │ (None, 32, 10) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ lstm_1 (LSTM) │ (None, 100) │ 44,400 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 64) │ 6,464 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ output_dense_td_layer (Dense) │ (None, 24) │ 1,560 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ reshape (Reshape) │ (None, 24, 1) │ 0 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 52,424 (204.78 KB)
Trainable params: 52,424 (204.78 KB)
Non-trainable params: 0 (0.00 B)
Personalización avanzada
Todos los argumentos que controlan el tipo de capas, número de unidades, funciones de activación y otras opciones pueden personalizarse. Si necesitas una flexibilidad total, también puedes pasar tu propio modelo de Keras para usarlo directamente, en vez de crearlo con la función auxiliar.
Los argumentos recurrent_layers_kwargs
y dense_layers_kwargs
te permiten especificar los parámetros para las capas recurrentes y densas, respectivamente.
Si usas un diccionario, estos kwargs se aplican a todas las capas del mismo tipo. Por ejemplo, si defines
recurrent_layers_kwargs = {'activation': 'tanh'}
, todas las capas recurrentes usarán la función de activacióntanh
.También puedes pasar una lista de diccionarios para indicar parámetros diferentes en cada capa. Por ejemplo,
recurrent_layers_kwargs = [{'activation': 'tanh'}, {'activation': 'relu'}]
especifica que la primera capa recurrente usarátanh
y la segundarelu
.
# Uso avanzado de `create_and_compile_model`
# ==============================================================================
model = create_and_compile_model(
series = data,
levels = ["o3"],
lags = 32,
steps = 24,
exog = None, # Sin variables exógenas
recurrent_layer = "LSTM",
recurrent_units = [128, 64],
recurrent_layers_kwargs = [{'activation': 'tanh'}, {'activation': 'relu'}],
dense_units = [128, 64],
dense_layers_kwargs = {'activation': 'relu'},
output_dense_layer_kwargs = {'activation': 'linear'},
compile_kwargs = {'optimizer': Adam(learning_rate=0.001), 'loss': MeanSquaredError()},
model_name = None
)
model.summary()
keras version: 3.10.0 Using backend: tensorflow tensorflow version: 2.19.0
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ series_input (InputLayer) │ (None, 32, 10) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ lstm_1 (LSTM) │ (None, 32, 128) │ 71,168 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ lstm_2 (LSTM) │ (None, 64) │ 49,408 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 128) │ 8,320 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 64) │ 8,256 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ output_dense_td_layer (Dense) │ (None, 24) │ 1,560 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ reshape_1 (Reshape) │ (None, 24, 1) │ 0 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 138,712 (541.84 KB)
Trainable params: 138,712 (541.84 KB)
Non-trainable params: 0 (0.00 B)
Para entender esta función en mayor profundidad, consulta la sección correspondiente en esta guía: Entendiendo create_and_compile_model
en profundidad.
Si necesitas definir una arquitectura completamente personalizada, puedes crear tu propio modelo de Keras y usarlo directamente en skforecast.
# Arquitectura del modelo (requiere `pydot` and `graphviz`)
# ==============================================================================
# from keras.utils import plot_model
# plot_model(model, show_shapes=True, show_layer_names=True, to_file='model-architecture.png')
Una vez que el modelo ha sido creado y compilado, el siguiente paso es crear una instancia de ForecasterRnn. Esta clase se encarga de añadir al modelo de deep learning todas las funcionalidades necesarias para que pueda usarse en problemas de predicción de series temporales. Además, es compatible con el resto de las funcionalidades que ofrece skforecast (backtesting, variables exógenas, etc.).
Problemas 1:1 — Serie única, salida única¶
En este escenario, el objetivo es predecir el siguiente valor de una única serie temporal, utilizando solo sus propias observaciones pasadas como predictors. Este tipo de problema se conoce como predicción autorregresiva univariante.
Por ejemplo: Dada una secuencia de valores ${y_{t-3}, y_{t-2}, y_{t-1}}$, predecir $y_{t+1}$.
Single-step forecasting¶
Este es el caso más sencillo para la predicción con redes neuronales recurrentes: tanto el entrenamiento como la predicción se basan en una única serie temporal. En este caso, simplemente hay que pasar esa serie al argumento series
de la función create_and_compile_model
, y establecer esa misma serie como objetivo mediante el argumento levels
. Como se desea predecir solo un valor en el futuro, el parámetro steps
debe fijarse en 1.
# Crear modelo
# ==============================================================================
lags = 24
model = create_and_compile_model(
series = data[["o3"]], # Solo la serie 'o3' se usa como predictor
levels = ["o3"], # Serie a predecir
lags = lags, # Número de lags a usar como predictores
steps = 1, # Single-step forecasting
recurrent_layer = "GRU",
recurrent_units = 64,
recurrent_layers_kwargs = {"activation": "tanh"},
dense_units = 32,
compile_kwargs = {'optimizer': Adam(), 'loss': MeanSquaredError()},
model_name = "Single-Series-Single-Step"
)
model.summary()
keras version: 3.10.0 Using backend: tensorflow tensorflow version: 2.19.0
Model: "Single-Series-Single-Step"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ series_input (InputLayer) │ (None, 24, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ gru_1 (GRU) │ (None, 64) │ 12,864 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 32) │ 2,080 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ output_dense_td_layer (Dense) │ (None, 1) │ 33 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ reshape_2 (Reshape) │ (None, 1, 1) │ 0 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 14,977 (58.50 KB)
Trainable params: 14,977 (58.50 KB)
Non-trainable params: 0 (0.00 B)
El forecaster se crea a partir del modelo y se le proporciona un conjunto de datos de validación para poder evaluar el rendimiento del modelo en cada época de entrenamiento. Además, se utiliza un MinMaxScaler
para estandarizar los datos de entrada y salida. Este objeto se encarga de transformar tanto los datos de entrenamiento como las predicciones, asegurando que los resultados se devuelvan a su escala original.
El diccionario fit_kwargs
contiene los parámetros que se pasan al método fit
del modelo. En este ejemplo, se especifican el número de épocas de entrenamiento, el tamaño del batch, los datos de validación y un callback de EarlyStopping
, que detiene el entrenamiento si la pérdida de validación no mejora.
# Crear el Forecaster
# ==============================================================================
forecaster = ForecasterRnn(
regressor=model,
levels=["o3"],
lags=lags, # Debe coincidir con el número de lags usados en el modelo
transformer_series=MinMaxScaler(),
fit_kwargs={
"epochs": 25, # Número de épocas para entrenar el modelo.
"batch_size": 512, # Tamaño del batch para entrenar el modelo.
"callbacks": [
EarlyStopping(monitor="val_loss", patience=3, restore_best_weights=True)
], # Callback para detener el entrenamiento cuando ya no esté aprendiendo más.
"series_val": data_val, # Datos de validación para el entrenamiento del modelo.
},
)
# Entrenar el forecaster
# ==============================================================================
forecaster.fit(data_train[['o3']])
forecaster
Epoch 1/25
39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 35ms/step - loss: 0.0709 - val_loss: 0.0107 Epoch 2/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 32ms/step - loss: 0.0107 - val_loss: 0.0084 Epoch 3/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 30ms/step - loss: 0.0082 - val_loss: 0.0067 Epoch 4/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 30ms/step - loss: 0.0068 - val_loss: 0.0065 Epoch 5/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 35ms/step - loss: 0.0062 - val_loss: 0.0058 Epoch 6/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 35ms/step - loss: 0.0058 - val_loss: 0.0055 Epoch 7/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 35ms/step - loss: 0.0055 - val_loss: 0.0056 Epoch 8/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 35ms/step - loss: 0.0052 - val_loss: 0.0054 Epoch 9/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 35ms/step - loss: 0.0055 - val_loss: 0.0055 Epoch 10/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 36ms/step - loss: 0.0052 - val_loss: 0.0054 Epoch 11/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 1s 35ms/step - loss: 0.0054 - val_loss: 0.0055
ForecasterRnn
General Information
- Regressor: Functional
- Layers names: ['series_input', 'gru_1', 'dense_1', 'output_dense_td_layer', 'reshape_2']
- Lags: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
- Window size: 24
- Maximum steps to predict: [1]
- Exogenous included: False
- Creation date: 2025-07-28 16:38:10
- Last fit date: 2025-07-28 16:38:25
- Keras backend: tensorflow
- Skforecast version: 0.17.0
- Python version: 3.12.11
- Forecaster id: None
Exogenous Variables
-
None
Data Transformations
- Transformer for series: MinMaxScaler()
- Transformer for exog: MinMaxScaler()
Training Information
- Series names: o3
- Target series (levels): ['o3']
- Training range: [Timestamp('2019-01-01 00:00:00'), Timestamp('2021-03-31 23:00:00')]
- Training index type: DatetimeIndex
- Training index frequency: h
Regressor Parameters
-
{'name': 'Single-Series-Single-Step', 'trainable': True, 'layers': [{'module': 'keras.layers', 'class_name': 'InputLayer', 'config': {'batch_shape': (None, 24, 1), 'dtype': 'float32', 'sparse': False, 'ragged': False, 'name': 'series_input'}, 'registered_name': None, 'name': 'series_input', 'inbound_nodes': []}, {'module': 'keras.layers', 'class_name': 'GRU', 'config': {'name': 'gru_1', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'return_sequences': False, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'zero_output_for_mask': False, 'units': 64, 'activation': 'tanh', 'recurrent_activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'recurrent_initializer': {'module': 'keras.initializers', 'class_name': 'Orthogonal', 'config': {'seed': None, 'gain': 1.0}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'recurrent_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'recurrent_constraint': None, 'bias_constraint': None, 'dropout': 0.0, 'recurrent_dropout': 0.0, 'reset_after': True, 'seed': None}, 'registered_name': None, 'build_config': {'input_shape': [None, 24, 1]}, 'name': 'gru_1', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 24, 1), 'dtype': 'float32', 'keras_history': ['series_input', 0, 0]}},), 'kwargs': {'training': False, 'mask': None}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 32, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': [None, 64]}, 'name': 'dense_1', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 64), 'dtype': 'float32', 'keras_history': ['gru_1', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'output_dense_td_layer', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 1, 'activation': 'linear', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': [None, 32]}, 'name': 'output_dense_td_layer', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 32), 'dtype': 'float32', 'keras_history': ['dense_1', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Reshape', 'config': {'name': 'reshape_2', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'target_shape': (1, 1)}, 'registered_name': None, 'build_config': {'input_shape': [None, 1]}, 'name': 'reshape_2', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 1), 'dtype': 'float32', 'keras_history': ['output_dense_td_layer', 0, 0]}},), 'kwargs': {}}]}], 'input_layers': [['series_input', 0, 0]], 'output_layers': [['reshape_2', 0, 0]]}
Compile Parameters
-
{'optimizer': {'module': 'keras.optimizers', 'class_name': 'Adam', 'config': {'name': 'adam', 'learning_rate': 0.0010000000474974513, 'weight_decay': None, 'clipnorm': None, 'global_clipnorm': None, 'clipvalue': None, 'use_ema': False, 'ema_momentum': 0.99, 'ema_overwrite_frequency': None, 'loss_scale_factor': None, 'gradient_accumulation_steps': None, 'beta_1': 0.9, 'beta_2': 0.999, 'epsilon': 1e-07, 'amsgrad': False}, 'registered_name': None}, 'loss': {'module': 'keras.losses', 'class_name': 'MeanSquaredError', 'config': {'name': 'mean_squared_error', 'reduction': 'sum_over_batch_size'}, 'registered_name': None}, 'loss_weights': None, 'metrics': None, 'weighted_metrics': None, 'run_eagerly': False, 'steps_per_execution': 1, 'jit_compile': False}
Fit Kwargs
-
{'epochs': 25, 'batch_size': 512, 'callbacks': [
✎ Note
La librería **skforecast** es totalmente compatible con GPUs. Consulta la sección **Ejecutando en GPU** más abajo en este documento para más información.En los modelos de deep learning es importante controlar el sobreajuste (overfitting), que ocurre cuando un modelo obtiene buenos resultados con los datos de entrenamiento pero un rendimiento pobre con datos nuevos o no vistos. Una estrategia habitual para evitarlo es utilizar un callback de Keras, como EarlyStopping
, que detiene el entrenamiento si la pérdida de validación deja de mejorar.
Otra práctica muy útil es visualizar la pérdida de entrenamiento y validación después de cada época. Esto te permite ver cómo está aprendiendo el modelo y detectar posibles síntomas de sobreajuste.
Explicación gráfica del sobreajuste. Fuente: https://datahacker.rs/018-pytorch-popular-techniques-to-prevent-the-overfitting-in-a-neural-networks/.
# Seguimiento del entrenamiento y overfitting
# ==============================================================================
fig, ax = plt.subplots(figsize=(8, 3))
_ = forecaster.plot_history(ax=ax)
En la gráfica anterior, la pérdida de entrenamiento (azul) disminuye rápidamente durante las dos primeras épocas, lo que indica que el modelo está capturando rápidamente los patrones principales de los datos. La pérdida de validación (rojo) comienza baja y se mantiene estable a lo largo del proceso de entrenamiento, siguiendo de cerca la pérdida de entrenamiento. Esto sugiere que:
El modelo no está sobreajustando, ya que la pérdida de validación se mantiene cercana a la de entrenamiento en todas las épocas.
Ambas pérdidas disminuyen y se estabilizan juntas, lo que indica una buena generalización y un aprendizaje efectivo.
No se observa divergencia, que aparecería si la pérdida de validación aumentara mientras la de entrenamiento sigue disminuyendo.
Una vez que el forecaster ha sido entrenado, se pueden obtener las predicciones. Si el parámetro steps
es None
en el método predict
, el forecaster predecirá todos los pasos futuros aprendidos, forecaster.max_step
.
# Forecaster steps disponibles
# ==============================================================================
forecaster.max_step
np.int64(1)
# Predicción
# ==============================================================================
predictions = forecaster.predict(steps=None) # Igual que steps=1
predictions
level | pred | |
---|---|---|
2021-04-01 | o3 | 48.03727 |
Para obtener una estimación robusta de la capacidad predictiva del modelo, se realiza un proceso de backtesting. El proceso de backtesting consiste en generar una predicción para cada observación del conjunto de test, siguiendo el mismo procedimiento que se seguiría si el modelo estuviese en producción, y finalmente comparar el valor predicho con el valor real.
# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(
steps = forecaster.max_step,
initial_train_size = len(data.loc[:end_validation, :]), # Training + Validation Data
refit = False
)
metrics, predictions = backtesting_forecaster_multiseries(
forecaster = forecaster,
series = data[['o3']],
cv = cv,
levels = forecaster.levels,
metric = "mean_absolute_error",
verbose = False # Set to True for detailed output
)
Epoch 1/25
48/48 ━━━━━━━━━━━━━━━━━━━━ 3s 38ms/step - loss: 0.0055 - val_loss: 0.0055 Epoch 2/25 48/48 ━━━━━━━━━━━━━━━━━━━━ 2s 36ms/step - loss: 0.0053 - val_loss: 0.0054 Epoch 3/25 48/48 ━━━━━━━━━━━━━━━━━━━━ 2s 36ms/step - loss: 0.0053 - val_loss: 0.0054 Epoch 4/25 48/48 ━━━━━━━━━━━━━━━━━━━━ 2s 35ms/step - loss: 0.0053 - val_loss: 0.0061 Epoch 5/25 48/48 ━━━━━━━━━━━━━━━━━━━━ 2s 35ms/step - loss: 0.0055 - val_loss: 0.0053 Epoch 6/25 48/48 ━━━━━━━━━━━━━━━━━━━━ 2s 35ms/step - loss: 0.0053 - val_loss: 0.0054 Epoch 7/25 48/48 ━━━━━━━━━━━━━━━━━━━━ 2s 35ms/step - loss: 0.0053 - val_loss: 0.0053 Epoch 8/25 48/48 ━━━━━━━━━━━━━━━━━━━━ 2s 35ms/step - loss: 0.0052 - val_loss: 0.0062
0%| | 0/2208 [00:00<?, ?it/s]
# Métrica de backtesting
# ==============================================================================
metrics
levels | mean_absolute_error | |
---|---|---|
0 | o3 | 6.046513 |
# Predicciones de backtesting
# ==============================================================================
predictions.head(4)
level | pred | |
---|---|---|
2021-10-01 00:00:00 | o3 | 55.243050 |
2021-10-01 01:00:00 | o3 | 60.001530 |
2021-10-01 02:00:00 | o3 | 64.217644 |
2021-10-01 03:00:00 | o3 | 64.490860 |
# Gráfico de las predicciones vs valores reales en el conjunto de test
# ==============================================================================
fig = go.Figure()
trace1 = go.Scatter(x=data_test.index, y=data_test['o3'], name="test", mode="lines")
trace2 = go.Scatter(
x=predictions.index,
y=predictions.loc[predictions["level"] == "o3", "pred"],
name="predictions", mode="lines"
)
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
title="Prediction vs real values in the test set",
xaxis_title="Date time",
yaxis_title="O3",
width=800,
height=400,
margin=dict(l=20, r=20, t=35, b=20),
legend=dict(orientation="h", yanchor="top", y=1.05, xanchor="left", x=0)
)
fig.show()
# Error en % respecto a la media de la serie
# ==============================================================================
rel_mse = 100 * metrics.loc[0, 'mean_absolute_error'] / np.mean(data["o3"])
print(f"Media de la serie: {np.mean(data['o3']):0.2f}")
print(f"Error (mae) relativo: {rel_mse:0.2f} %")
Media de la serie: 54.52 Error (mae) relativo: 11.09 %
Multi-step forecasting¶
En este caso, el objetivo es predecir varios valores futuros de una única serie temporal utilizando solo sus propias observaciones pasadas como predictores. A esto se le denomina predicción univariante multi-paso.
Por ejemplo: Dada una secuencia de valores ${y_{t-24}, ..., y_{t-1}}$, predecir ${y_{t+1}, y_{t+2}, ..., y_{t+h}}$, donde $n$ es el horizonte de predicción (número de pasos a futuro).
Esta configuración es habitual cuando se quiere predecir varios pasos a futuro (por ejemplo, las próximas 24 horas de concentración de ozono).
Arquitectura del modelo
Puedes emplear una arquitectura de red similar a la del caso de un solo paso, pero predecir varios pasos en el futuro suele beneficiarse de aumentar la capacidad del modelo (por ejemplo, añadiendo más unidades en las capas LSTM/GRU o capas densas adicionales). Esto permite al modelo capturar mejor la complejidad de anticipar varios valores a la vez.
# Create model
# ==============================================================================
lags = 24
model = create_and_compile_model(
series = data[["o3"]], # Solo la serie 'o3' se usa como predictor
levels = ["o3"], # Serie a predecir
lags = lags, # Número de lags a usar como predictores
steps = 24, # Multi-step forecasting
recurrent_layer = "GRU",
recurrent_units = 128,
recurrent_layers_kwargs = {"activation": "tanh"},
dense_units = 64,
compile_kwargs = {'optimizer': 'adam', 'loss': 'mse'},
model_name = "Single-Series-Multi-Step"
)
model.summary()
keras version: 3.10.0 Using backend: tensorflow tensorflow version: 2.19.0
Model: "Single-Series-Multi-Step"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ series_input (InputLayer) │ (None, 24, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ gru_1 (GRU) │ (None, 128) │ 50,304 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 64) │ 8,256 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ output_dense_td_layer (Dense) │ (None, 24) │ 1,560 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ reshape_3 (Reshape) │ (None, 24, 1) │ 0 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 60,120 (234.84 KB)
Trainable params: 60,120 (234.84 KB)
Non-trainable params: 0 (0.00 B)
✎ Note
El parámetrofit_kwargs
permite personalizar cualquier aspecto del proceso de entrenamiento del modelo, pasando argumentos directamente al método Model.fit()
de Keras. Por ejemplo, puedes especificar el número de épocas de entrenamiento, el tamaño del batch y cualquier callback que desees utilizar.
En el ejemplo, el modelo se entrena durante 50 épocas con un batch size de 512. El callback EarlyStopping
monitoriza la pérdida de validación y detiene automáticamente el entrenamiento si no mejora durante 3 épocas consecutivas (patience=3
). Esto ayuda a prevenir el sobreajuste y a ahorrar tiempo de computación.
También puedes añadir otros callbacks, como ModelCheckpoint
para guardar el modelo en cada época, o TensorBoard
para visualizar en tiempo real las métricas de entrenamiento y validación.
# Crear el Forecaster
# ==============================================================================
forecaster = ForecasterRnn(
regressor=model,
levels=["o3"],
lags=lags, # Debe coincidir con el número de lags usados en el modelo
transformer_series=MinMaxScaler(),
fit_kwargs={
"epochs": 25, # Número de épocas para entrenar el modelo.
"batch_size": 512, # Tamaño del batch para entrenar el modelo.
"callbacks": [
EarlyStopping(monitor="val_loss", patience=3, restore_best_weights=True)
], # Callback para detener el entrenamiento cuando ya no esté aprendiendo más.
"series_val": data_val, # Datos de validación para el entrenamiento del modelo.
},
)
# Entrenar el forecaster
# ==============================================================================
forecaster.fit(data_train[['o3']])
forecaster
Epoch 1/25
39/39 ━━━━━━━━━━━━━━━━━━━━ 3s 66ms/step - loss: 0.1309 - val_loss: 0.0319 Epoch 2/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 61ms/step - loss: 0.0297 - val_loss: 0.0262 Epoch 3/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 63ms/step - loss: 0.0263 - val_loss: 0.0241 Epoch 4/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 61ms/step - loss: 0.0248 - val_loss: 0.0221 Epoch 5/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 61ms/step - loss: 0.0232 - val_loss: 0.0195 Epoch 6/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 62ms/step - loss: 0.0214 - val_loss: 0.0181 Epoch 7/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 62ms/step - loss: 0.0203 - val_loss: 0.0178 Epoch 8/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 61ms/step - loss: 0.0200 - val_loss: 0.0172 Epoch 9/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 62ms/step - loss: 0.0197 - val_loss: 0.0173 Epoch 10/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 62ms/step - loss: 0.0193 - val_loss: 0.0171 Epoch 11/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 62ms/step - loss: 0.0190 - val_loss: 0.0169 Epoch 12/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 63ms/step - loss: 0.0188 - val_loss: 0.0168 Epoch 13/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 63ms/step - loss: 0.0186 - val_loss: 0.0166 Epoch 14/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 61ms/step - loss: 0.0183 - val_loss: 0.0162 Epoch 15/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 62ms/step - loss: 0.0180 - val_loss: 0.0164 Epoch 16/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 62ms/step - loss: 0.0180 - val_loss: 0.0164 Epoch 17/25 39/39 ━━━━━━━━━━━━━━━━━━━━ 2s 61ms/step - loss: 0.0178 - val_loss: 0.0169
ForecasterRnn
General Information
- Regressor: Functional
- Layers names: ['series_input', 'gru_1', 'dense_1', 'output_dense_td_layer', 'reshape_3']
- Lags: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
- Window size: 24
- Maximum steps to predict: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
- Exogenous included: False
- Creation date: 2025-07-28 16:41:03
- Last fit date: 2025-07-28 16:41:46
- Keras backend: tensorflow
- Skforecast version: 0.17.0
- Python version: 3.12.11
- Forecaster id: None
Exogenous Variables
-
None
Data Transformations
- Transformer for series: MinMaxScaler()
- Transformer for exog: MinMaxScaler()
Training Information
- Series names: o3
- Target series (levels): ['o3']
- Training range: [Timestamp('2019-01-01 00:00:00'), Timestamp('2021-03-31 23:00:00')]
- Training index type: DatetimeIndex
- Training index frequency: h
Regressor Parameters
-
{'name': 'Single-Series-Multi-Step', 'trainable': True, 'layers': [{'module': 'keras.layers', 'class_name': 'InputLayer', 'config': {'batch_shape': (None, 24, 1), 'dtype': 'float32', 'sparse': False, 'ragged': False, 'name': 'series_input'}, 'registered_name': None, 'name': 'series_input', 'inbound_nodes': []}, {'module': 'keras.layers', 'class_name': 'GRU', 'config': {'name': 'gru_1', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'return_sequences': False, 'return_state': False, 'go_backwards': False, 'stateful': False, 'unroll': False, 'zero_output_for_mask': False, 'units': 128, 'activation': 'tanh', 'recurrent_activation': 'sigmoid', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'recurrent_initializer': {'module': 'keras.initializers', 'class_name': 'Orthogonal', 'config': {'seed': None, 'gain': 1.0}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'recurrent_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'recurrent_constraint': None, 'bias_constraint': None, 'dropout': 0.0, 'recurrent_dropout': 0.0, 'reset_after': True, 'seed': None}, 'registered_name': None, 'build_config': {'input_shape': [None, 24, 1]}, 'name': 'gru_1', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 24, 1), 'dtype': 'float32', 'keras_history': ['series_input', 0, 0]}},), 'kwargs': {'training': False, 'mask': None}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 64, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': [None, 128]}, 'name': 'dense_1', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 128), 'dtype': 'float32', 'keras_history': ['gru_1', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Dense', 'config': {'name': 'output_dense_td_layer', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'units': 24, 'activation': 'linear', 'use_bias': True, 'kernel_initializer': {'module': 'keras.initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': None}, 'bias_initializer': {'module': 'keras.initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': None}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}, 'registered_name': None, 'build_config': {'input_shape': [None, 64]}, 'name': 'output_dense_td_layer', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 64), 'dtype': 'float32', 'keras_history': ['dense_1', 0, 0]}},), 'kwargs': {}}]}, {'module': 'keras.layers', 'class_name': 'Reshape', 'config': {'name': 'reshape_3', 'trainable': True, 'dtype': {'module': 'keras', 'class_name': 'DTypePolicy', 'config': {'name': 'float32'}, 'registered_name': None}, 'target_shape': (24, 1)}, 'registered_name': None, 'build_config': {'input_shape': [None, 24]}, 'name': 'reshape_3', 'inbound_nodes': [{'args': ({'class_name': '__keras_tensor__', 'config': {'shape': (None, 24), 'dtype': 'float32', 'keras_history': ['output_dense_td_layer', 0, 0]}},), 'kwargs': {}}]}], 'input_layers': [['series_input', 0, 0]], 'output_layers': [['reshape_3', 0, 0]]}
Compile Parameters
-
{'optimizer': {'module': 'keras.optimizers', 'class_name': 'Adam', 'config': {'name': 'adam', 'learning_rate': 0.0010000000474974513, 'weight_decay': None, 'clipnorm': None, 'global_clipnorm': None, 'clipvalue': None, 'use_ema': False, 'ema_momentum': 0.99, 'ema_overwrite_frequency': None, 'loss_scale_factor': None, 'gradient_accumulation_steps': None, 'beta_1': 0.9, 'beta_2': 0.999, 'epsilon': 1e-07, 'amsgrad': False}, 'registered_name': None}, 'loss': 'mse', 'loss_weights': None, 'metrics': None, 'weighted_metrics': None, 'run_eagerly': False, 'steps_per_execution': 1, 'jit_compile': False}
Fit Kwargs
-
{'epochs': 25, 'batch_size': 512, 'callbacks': [
# Seguimiento del entrenamiento y overfitting
# ==============================================================================
fig, ax = plt.subplots(figsize=(8, 3))
_ = forecaster.plot_history(ax=ax)
En este caso, se espera que la calidad de las predicciones sea inferior a la del ejemplo anterior, como se observa en los valores más altos de la pérdida a lo largo de las épocas. La explicación es sencilla: ahora el modelo tiene que predecir 24 valores en cada paso, en lugar de solo 1. Por tanto, la pérdida de validación es mayor, ya que refleja el error combinado de las 24 predicciones, en vez del error de una sola predicción.
# Forecaster steps disponibles
# ==============================================================================
forecaster.max_step
np.int64(24)
# Predicción
# ==============================================================================
predictions = forecaster.predict(steps=24) # Igual que steps=None
predictions
level | pred | |
---|---|---|
2021-04-01 00:00:00 | o3 | 53.218834 |
2021-04-01 01:00:00 | o3 | 45.867271 |
2021-04-01 02:00:00 | o3 | 49.176682 |
2021-04-01 03:00:00 | o3 | 42.983650 |
2021-04-01 04:00:00 | o3 | 33.996475 |
2021-04-01 05:00:00 | o3 | 33.777569 |
2021-04-01 06:00:00 | o3 | 25.119589 |
2021-04-01 07:00:00 | o3 | 26.604334 |
2021-04-01 08:00:00 | o3 | 32.534935 |
2021-04-01 09:00:00 | o3 | 35.588940 |
2021-04-01 10:00:00 | o3 | 50.552128 |
2021-04-01 11:00:00 | o3 | 59.708046 |
2021-04-01 12:00:00 | o3 | 74.053825 |
2021-04-01 13:00:00 | o3 | 78.300232 |
2021-04-01 14:00:00 | o3 | 87.985336 |
2021-04-01 15:00:00 | o3 | 88.108139 |
2021-04-01 16:00:00 | o3 | 88.268661 |
2021-04-01 17:00:00 | o3 | 83.062645 |
2021-04-01 18:00:00 | o3 | 79.872185 |
2021-04-01 19:00:00 | o3 | 72.618408 |
2021-04-01 20:00:00 | o3 | 70.120392 |
2021-04-01 21:00:00 | o3 | 64.149719 |
2021-04-01 22:00:00 | o3 | 65.045151 |
2021-04-01 23:00:00 | o3 | 60.506474 |
También se pueden predecir steps
especificos, siempre y cuando se encuentren dentro del horizonte de predicción definido en el modelo.
# Predicción steps especificos
# ==============================================================================
predictions = forecaster.predict(steps=[1, 3])
predictions
level | pred | |
---|---|---|
2021-04-01 00:00:00 | o3 | 53.218834 |
2021-04-01 02:00:00 | o3 | 49.176682 |
# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(
steps = forecaster.max_step,
initial_train_size = len(data.loc[:end_validation, :]), # Training + Validation Data
refit = False
)
metrics, predictions = backtesting_forecaster_multiseries(
forecaster = forecaster,
series = data[['o3']],
cv = cv,
levels = forecaster.levels,
metric = "mean_absolute_error",
verbose = False,
suppress_warnings = True
)
Epoch 1/25
47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 77ms/step - loss: 0.0178 - val_loss: 0.0166 Epoch 2/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 73ms/step - loss: 0.0177 - val_loss: 0.0160 Epoch 3/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 72ms/step - loss: 0.0175 - val_loss: 0.0160 Epoch 4/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 75ms/step - loss: 0.0176 - val_loss: 0.0160 Epoch 5/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 77ms/step - loss: 0.0172 - val_loss: 0.0158 Epoch 6/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 88ms/step - loss: 0.0173 - val_loss: 0.0158 Epoch 7/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 80ms/step - loss: 0.0172 - val_loss: 0.0160 Epoch 8/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 73ms/step - loss: 0.0171 - val_loss: 0.0163 Epoch 9/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 73ms/step - loss: 0.0171 - val_loss: 0.0157 Epoch 10/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 78ms/step - loss: 0.0170 - val_loss: 0.0161 Epoch 11/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 74ms/step - loss: 0.0171 - val_loss: 0.0156 Epoch 12/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 68ms/step - loss: 0.0169 - val_loss: 0.0158 Epoch 13/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 69ms/step - loss: 0.0168 - val_loss: 0.0156 Epoch 14/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 69ms/step - loss: 0.0166 - val_loss: 0.0155 Epoch 15/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 4s 76ms/step - loss: 0.0167 - val_loss: 0.0157 Epoch 16/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 73ms/step - loss: 0.0167 - val_loss: 0.0159 Epoch 17/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 68ms/step - loss: 0.0166 - val_loss: 0.0155 Epoch 18/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 73ms/step - loss: 0.0168 - val_loss: 0.0158 Epoch 19/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 68ms/step - loss: 0.0165 - val_loss: 0.0155 Epoch 20/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 71ms/step - loss: 0.0165 - val_loss: 0.0158 Epoch 21/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 70ms/step - loss: 0.0165 - val_loss: 0.0156 Epoch 22/25 47/47 ━━━━━━━━━━━━━━━━━━━━ 3s 69ms/step - loss: 0.0165 - val_loss: 0.0157
0%| | 0/92 [00:00<?, ?it/s]
# Backtesting metrics
# ==============================================================================
metric_single_series = metrics.loc[metrics["levels"] == "o3", "mean_absolute_error"].iat[0]
metrics
levels | mean_absolute_error | |
---|---|---|
0 | o3 | 11.022597 |
# Predicciones de backtesting
# ==============================================================================
predictions
level | pred | |
---|---|---|
2021-10-01 00:00:00 | o3 | 54.378468 |
2021-10-01 01:00:00 | o3 | 54.153458 |
2021-10-01 02:00:00 | o3 | 52.569321 |
2021-10-01 03:00:00 | o3 | 48.884068 |
2021-10-01 04:00:00 | o3 | 47.485912 |
... | ... | ... |
2021-12-31 19:00:00 | o3 | 18.336298 |
2021-12-31 20:00:00 | o3 | 19.118999 |
2021-12-31 21:00:00 | o3 | 22.968521 |
2021-12-31 22:00:00 | o3 | 25.472700 |
2021-12-31 23:00:00 | o3 | 29.325987 |
2208 rows × 2 columns
# Gráfico de las predicciones vs valores reales en el conjunto de test
# ==============================================================================
fig = go.Figure()
trace1 = go.Scatter(x=data_test.index, y=data_test['o3'], name="test", mode="lines")
trace2 = go.Scatter(
x=predictions.index,
y=predictions.loc[predictions["level"] == "o3", "pred"],
name="predictions", mode="lines"
)
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
title="Prediction vs real values in the test set",
xaxis_title="Date time",
yaxis_title="O3",
width=800,
height=400,
margin=dict(l=20, r=20, t=35, b=20),
legend=dict(orientation="h", yanchor="top", y=1.05, xanchor="left", x=0)
)
fig.show()