Si te gusta Skforecast , ayúdanos dándonos una estrella en GitHub! ⭐️
Más sobre forecasting en: cienciadedatos.net
En casos de uso en los que se necesita predecir cientos o miles de series temporales ¿se debe desarrollar un modelo individual para cada serie o un único modelo capaz de predecir todas las series a la vez?
En los modelo de forecasting locales, se crea un modelo independiente para cada serie temporal. Aunque este método proporciona una comprensión exhaustiva de cada serie, su escalabilidad puede verse dificultada por la necesidad de crear y mantener cientos o miles de modelos.
El modelo de forecasting global consiste en crear un único modelo que tenga en cuenta todas las series temporales simultáneamente. Intenta captar los patrones comunes que rigen las series, mitigando así el ruido que pueda introducir cada serie. Este enfoque es eficiente desde el punto de vista computacional, fácil de mantener y puede producir generalizaciones más sólidas, aunque potencialmente a costa de sacrificar algunos aspectos individuales.
Para ayudar a comprender las ventajas de cada estrategia de forecasting, este documento examina y compara los resultados obtenidos al predecir el consumo energético de más de mil edificios utilizando el conjunto de datos ASHRAE - Great Energy Predictor III disponible en Kaggle.
El forecasting con modelos globales parte de la base de que las series que se comportan de forma similar pueden beneficiarse de ser modelizadas conjuntamente. Aunque el uso principal de cada edificio está disponible en el conjunto de datos, puede que no refleje grupos con patrones similares de consumo de energía, por lo que se crean grupos adicionales utilizando métodos de clustering. Se realizan un total de 5 experimentos:
Forecasting individual de cada edificio.
Forecasting de todos los edificios juntos con una única estrategia de modelo global.
Forecasting de grupos de edificios en función de su uso principal (un modelo global por uso principal).
Forecasting de grupos de edificios basada en la agrupación de características de series temporales (un modelo global por agrupación).
Forecasting de grupos de edificios basada en la agrupación por Dynamic Time Warping (DTW) (un modelo global por agrupación).
El consumo de energía de cada edificio se predice semanalmente (resolución diaria) durante 13 semanas siguiendo cada estrategia. La eficacia de cada enfoque se evalúa utilizando varias métricas de rendimiento, como el error absoluto medio (MAE), el error absoluto y el sesgo. El objetivo del estudio es identificar el enfoque más eficaz tanto para las predicciones globales como para un grupo específico de edificios.
✎ Note
Si prefieres una visión general rápida antes de sumergirte en los detalles, considera comenzar con la sección de Conclusiones. Este enfoque te permite adaptar tu lectura a tus intereses y limitaciones de tiempo, y resume nuestros hallazgos e ideas. Después de leer las conclusiones, es posible que encuentres ciertas secciones particularmente relevantes o interesantes. Siéntete libre de navegar directamente a esas partes del artículo para una comprensión más profunda.✎ Note
Este documento se centra en un caso de uso. Para una descripción detallada de las muchas características que skforecast proporciona para la construcción de modelos de forecasting globales, consulta Modelos de forecasting globales: Modelado de múltiples series temporales con machine learning.Librerías utilizadas en este documento.
# Manipulación de datos
# ==============================================================================
import numpy as np
import pandas as pd
from datetime import datetime
# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
plt.style.use('seaborn-v0_8-darkgrid')
from tqdm.auto import tqdm
# Forecasting
# ==============================================================================
import lightgbm
from lightgbm import LGBMRegressor
from sklearn.preprocessing import StandardScaler
import skforecast
from skforecast.ForecasterAutoreg import ForecasterAutoreg
from skforecast.model_selection import backtesting_forecaster
from skforecast.ForecasterAutoregMultiSeries import ForecasterAutoregMultiSeries
from skforecast.model_selection_multiseries import backtesting_forecaster_multiseries
# Feature engineering
# ==============================================================================
import tsfresh
from tsfresh import extract_features
from tsfresh import select_features
from tsfresh.feature_extraction.settings import ComprehensiveFCParameters
from tsfresh.feature_extraction.settings import from_columns
# Clustering
# ==============================================================================
import sklearn
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
import tslearn
from tslearn.clustering import TimeSeriesKMeans
# Warnings
# ==============================================================================
import warnings
warnings.filterwarnings('once')
color = '\033[1m\033[38;5;208m'
print(f"{color}Versión skforecast: {skforecast.__version__}")
print(f"{color}Versión scikit-learn: {sklearn.__version__}")
print(f"{color}Versión lightgbm: {lightgbm.__version__}")
print(f"{color}Versión tsfresh: {tsfresh.__version__}")
print(f"{color}Versión tslearn: {tslearn.__version__}")
print(f"{color}Versión pandas: {pd.__version__}")
print(f"{color}Versión numpy: {np.__version__}")
⚠ Warning
En el momento de escribir este documento,tslearn
solo es compatible con versiones de numpy
inferiores a 2.0. Si tienes una versión superior, puedes reducirla ejecutando el siguiente comando: pip install numpy==1.26.4
Los datos utilizados en este documento se han obtenido de la competición de Kaggle Addison Howard, Chris Balbach, Clayton Miller, Jeff Haberl, Krishnan Gowri, Sohier Dane. (2019). ASHRAE - Great Energy Predictor III. Kaggle.
Tres archivos se utilizan para crear el conjunto de datos de modelado:
weather_train.csv y weather_test.csv: Estos archivos contienen datos relacionados con el clima para cada edificio, incluida la temperatura del aire exterior, la temperatura de rocío, la humedad relativa y otros parámetros meteorológicos. Los datos meteorológicos son cruciales para comprender el impacto de las condiciones externas en el uso de energía de los edificios.
building_metadata.csv: este archivo proporciona metadatos para cada edificio en el conjunto de datos, como el tipo de edificio, el uso principal, el metraje cuadrado, el número de pisos y el año de construcción. Esta información ayuda a comprender las características de los edificios y su posible influencia en los patrones de consumo de energía.
train.csv: el conjunto de datos de entrenamiento contiene la variable objetivo, es decir, los datos de consumo de energía para cada edificio, junto con las fechas de las lecturas de consumo de energía. También incluye los identificadores de edificio y clima correspondientes para vincular la información en diferentes conjuntos de datos.
Los tres archivos se han preprocesado para eliminar los edificios con menos del 85% de valores diferentes de NaN o cero, para utilizar solo el medidor de electricidad y para agregar los datos a una frecuencia diaria.
# Lectura de datos
# ==============================================================================
url = 'https://drive.google.com/uc?id=1fMsYjfhrFLmeFjKG3jenXjDa5s984ThC'
data = pd.read_parquet(url)
print("Data shape:", data.shape)
data.head()
# Imputar valores faltantes de air_temperature y wind_speed usando fowrard y backward fill
# ==============================================================================
# La imputación debe hacerse por separado para cada edificio
data = data.sort_values(by=['building_id', 'timestamp'])
data['air_temperature'] = data.groupby('building_id')['air_temperature'].ffill().bfill()
data['wind_speed'] = data.groupby('building_id')['wind_speed'].ffill().bfill()
data = data.sort_index()
print(
f"Rango de fechas disponibles : {data.index.min()} --- {data.index.max()} "
f"(n_días={(data.index.max() - data.index.min()).days})"
)
Uno de los atributos clave asociados con cada edificio es su uso designado. Esta característica puede desempeñar un papel crucial en la influencia del patrón de consumo de energía, ya que los usos distintos pueden impactar significativamente tanto en la cantidad como en el momento del consumo de energía.
# Número de edificios y tipo de edificios basado en el uso principal
# ==============================================================================
n_building = data['building_id'].nunique()
n_type_building = data['primary_use'].nunique()
range_datetime = [data.index.min(), data.index.max()]
print(f"Número de edificios: {n_building}")
print(f"Número de tipos de edificios: {n_type_building}")
display(data.drop_duplicates(subset=['building_id'])['primary_use'].value_counts())
Para algunas categorías de uso principal, hay un número limitado de edificios dentro del conjunto de datos. Para simplificar el análisis, las categorías con menos de 100 edificios se agrupan en la categoría "Other".
# Tipos de edificios (primary use) con menos de 100 muestras se agrupan como "Other".
# ==============================================================================
infrequent_categories = (
data
.drop_duplicates(subset=['building_id'])['primary_use']
.value_counts()
.loc[lambda x: x < 100]
.index
.tolist()
)
print(f"Infrequent categories:")
print("======================")
print('\n'.join(infrequent_categories))
data['primary_use'] = np.where(
data['primary_use'].isin(infrequent_categories),
'Other',
data['primary_use']
)
A continuación, se crea un gráfico que muestra el consumo de energía para un edificio seleccionado al azar dentro de cada categoría respectiva, y un gráfico de todas las series temporales disponibles para cada categoría.
# Series temporales para 1 edificio seleccionado al azar por grupo
# ==============================================================================
fig, axs = plt.subplots(nrows=3, ncols=2, figsize=(8, 5.5), sharex=True, sharey=False)
sample_ids = (
data
.groupby('primary_use')['building_id']
.apply(lambda x: x.sample(1, random_state=333))
.tolist()
)
axs = axs.flatten()
for i, building_id in enumerate(sample_ids):
data_sample = data[data['building_id'] == building_id]
building_type = data_sample['primary_use'].unique()[0]
data_sample.plot(
y = 'meter_reading',
ax = axs[i],
legend = False,
title = f"Edificio: {building_id}, tipo: {building_type}",
fontsize = 8
)
axs[i].set_xlabel("")
axs[i].set_ylabel("")
# Scientific notation for y axis
axs[i].ticklabel_format(axis='y', style='sci', scilimits=(0, 0))
axs[i].title.set_size(9)
fig.suptitle('Consumo de energía para 6 edificios aleatorios', fontsize=12)
fig.tight_layout()
plt.show()
# Consumo de energía por tipo de edificio (una línea gris por edificio)
# ==============================================================================
fig, axs = plt.subplots(nrows=3, ncols=2, figsize=(8, 5.5), sharex=True, sharey=False)
axs = axs.flatten()
for i, building_type in enumerate(data['primary_use'].unique()):
data_sample = data[data['primary_use'] == building_type]
data_sample = data_sample.pivot_table(
index = 'timestamp',
columns = 'building_id',
values = 'meter_reading',
aggfunc = 'mean'
)
data_sample.plot(
legend = False,
title = f"Tipo: {building_type}",
color = 'gray',
alpha = 0.2,
fontsize = 8,
ax = axs[i]
)
data_sample.mean(axis=1).plot(
ax = axs[i],
color = 'blue',
fontsize = 8
)
axs[i].set_xlabel("")
axs[i].set_ylabel("")
axs[i].ticklabel_format(axis='y', style='sci', scilimits=(0,0))
axs[i].title.set_size(9)
# Limitar el eje y a 5 veces el valor medio máximo para mejorar la visualización
axs[i].set_ylim(
bottom = 0,
top = 5 * data_sample.mean(axis=1).max()
)
fig.tight_layout()
fig.suptitle('Consumo de energía por tipo de edificio', fontsize=12)
plt.subplots_adjust(top=0.9)
plt.show()
El gráfico revela que hay una variabilidad notable en los patrones de consumo entre edificios del mismo propósito. Esto sugiere que puede haber margen para mejorar los criterios por los que se agrupan los edificios.
La idea detrás de modelar múltiples series de forma conjunta es poder capturar los patrones principales que rigen las series, reduciendo así el impacto del ruido que cada serie pueda tener. Esto significa que las series que se comportan de manera similar pueden beneficiarse de ser modelizadas juntas. Una forma de identificar posibles grupos de series es realizar un estudio de clustering antes de modelizar. Si como resultado del clustering se identifican grupos claros, es apropiado modelar cada uno de ellos por separado.
El clustering es una técnica de análisis no supervisado que agrupa un conjunto de observaciones en clústeres que contienen observaciones consideradas homogéneas, mientras que las observaciones en diferentes clústeres se consideran heterogéneas. Los algoritmos que agrupan series temporales se pueden dividir en dos grupos: aquellos que utilizan una transformación para crear variables antes de agrupar (clustering de series temporales basado en características) y aquellos que trabajan directamente en las series temporales (medidas de distancia elástica).
Clustering basado en características de series temporales: Se extraen varaibles que describen las características estructurales de cada serie temporal, y luego estas variables se introducen en algoritmos de clustering. Estas variables se obtienen aplicando operaciones estadísticas que capturan mejor las características subyacentes: tendencia, estacionalidad, periodicidad, correlación serial, asimetría, curtosis, caos, no linealidad y auto-similitud.
Medidas de distancia elástica: Este enfoque trabaja directamente en las series temporales, ajustando o "reajustando" las series en comparación con otras. La más conocida de esta familia de medidas es Dynamic Time Warping (DTW).
Para información más detallada sobre el clustering de series temporales, consulta A review and evaluation of elastic distance functions for time series clustering.
tsfresh es una librería de Python para extraer variables de series temporales y datos secuenciales, que incluye medidas estadísticas, coeficientes de Fourier y otras variables en el dominio del tiempo y de la frecuencia. Proporciona un enfoque sistemático para automatizar el cálculo de variables y seleccionar las más informativas.
Para empezar, se utiliza la configuración predeterminada de tsfresh, que calcula todas las variables disponibles. La forma más sencilla de acceder a esta configuración es utilizar las funciones proporcionadas por la clase ComprehensiveFCParameters
. Esta función crea un diccionario que asocia el nombre de cada característica a una lista de parámetros que se utilizarán cuando se llame a la función con ese nombre.
# Variables por defecto
# ==============================================================================
default_features = ComprehensiveFCParameters()
print("Nombre de las variables extraídas por tsfresh:")
print("===================================================")
print('\n'.join(list(default_features)))
Muchas de las variables se calculan utilizando diferentes valores para sus argumentos.
# Configuración por defecto para "partial_autocorrelation"
# ==============================================================================
default_features['partial_autocorrelation']
Para acceder a la vista detallada de cada característica y los valores de los parámetros incluidos en la configuración predeterminada, utilice el siguiente código:
# Configuración por defecto para todas las variables
# ==============================================================================
# from pprint import pprint
# pprint(default_features)
Una vez que se ha definido la configuración de las variables, el siguiente paso es extraerlas de las series temporales. Para ello, se utiliza la función extract_features()
de tsfresh. Esta función recibe como entrada la o las series temporales y la configuración de las variables a extraer. La salida es un dataframe con las variables extraídas.
# Extracción de variables
# ==============================================================================
ts_features = extract_features(
timeseries_container = data[['building_id', 'meter_reading']].reset_index(),
column_id = "building_id",
column_sort = "timestamp",
column_value = "meter_reading",
default_fc_parameters = default_features,
impute_function = tsfresh.utilities.dataframe_functions.impute,
n_jobs = 4
)
print("Dimensiones de ts_features:", ts_features.shape)
ts_features.head(3)
Como resultado del proceso de extracción, se han creado 783 variables para cada serie temporal (building_id
en este caso). El dataframe devuelto tiene como índice la columna especificada en el argumento column_id
de extract_features
.
La extracción por defecto de tsfresh genera un gran número de variables. Sin embargo, solo unas pocas pueden ser de interés en cada caso de uso. Para seleccionar las más relevantes, tsfresh incluye un proceso de selección automatizado basado en test de hipótesis (FeatuRE Extraction based on Scalable Hypothesis tests).
⚠ Warning
El proceso de selección utilizado por tsfresh se basa en la importancia de cada característica para predecir con exactitud la variable objetivo. Para realizar este proceso, se necesita una variable objetivo, por ejemplo, el tipo de edificio asociado a una serie temporal determinada. Sin embargo, hay casos en los que no se dispone fácilmente de una variable objetivo. En tales casos, pueden utilizarse estrategias alternativas:Como la selección de variables es un paso crítico que afecta a la información disponible para los siguientes pasos del análisis, es recomendable entender los parámetros que controlan el comportamiento de select_features()
.
X
: DataFrame con las variables creadas con extract_features
. Puede contener variables tanto binarias como de valor real al mismo tiempo.y
: Vector objetivo que se necesita para probar qué variables son relevantes. Puede ser binario o de valor real.test_for_binary_target_binary_feature
: Prueba que se utilizará cuando el objetivo es binario y la característica es binaria. Actualmente no se usa, valor predeterminado 'fisher'
.test_for_binary_target_real_feature
: Prueba que se utilizará cuando el objetivo es binario y la característica es continua (valor real). Valor predeterminado 'mann'
.test_for_real_target_binary_feature
: Prueba que se utilizará cuando el objetivo es continuo (valor real) y la característica es binaria. Actualmente no se usa, valor predeterminado 'mann'
.test_for_real_target_real_feature
: Prueba que se utilizará cuando el objetivo es continuo (valor real) y la característica es continua (valor real). Actualmente no se usa, valor predeterminado 'kendall'
.fdr_level
: El nivel de FDR que debe respetarse, este es el porcentaje teórico esperado de variables irrelevantes entre todas las variables creadas. Valor predeterminado 0.05
.hypotheses_independent
: ¿Se puede suponer que la significancia de las variables es independiente? Normalmente, esto debe establecerse en False, ya que las variables nunca son independientes (por ejemplo, media y mediana). Valor predeterminado False
.n_jobs
: Número de procesos a utilizar durante el cálculo del valor p. Valor predeterminado 4
.show_warnings
: Mostrar advertencias durante el cálculo del valor p (necesario para la depuración de los calculadores). Valor predeterminado False
.chunksize
: El tamaño de un chunk que se envía al proceso de trabajo para la paralelización. Donde un chunk se define como los datos para una característica. Si estableces el chunksize en 10, significa que una tarea es filtrar 10 variables. Si se establece en None
, dependiendo del distribuidor, se utilizan heurísticas para encontrar el tamaño óptimo de chunk. Si recibes excepciones de falta de memoria, puedes probar con el distribuidor dask y un chunksize más pequeño. Valor predeterminado None
.ml_task
: La tarea de machine learning prevista. Puede ser ‘classification’, ‘regression’ o ‘auto’. El valor predeterminado es ‘auto’, lo que significa que la tarea prevista se infiere a partir de y
. Si y
tiene un tipo de datos booleano, entero u objeto, se asume que la tarea es clasificación, de lo contrario, regresión. Valor predeterminado 'auto'
.multiclass
: Si el problema es de clasificación multiclase. Esto modifica la forma en que se seleccionan las variables. La clasificación multiclase requiere que las variables sean estadísticamente significativas para predecir n_significant
variables. Valor predeterminado False
.n_significant
: El número de clases para las cuales las variables deben ser predictores estadísticamente significativos para ser consideradas ‘relevantes’. Solo especificar cuando multiclass=True
. Valor predeterminado 1
.⚠ Warning
El orden del índice devuelto en el dataframe de variables no es el mismo que el orden de las columnas en el dataframe original. Por lo tanto, los datos pasados al argumentoy
en select_features
deben estar ordenados para garantizar la correcta asociación entre las variables y la variable objetivo.
# Seleccionar caracteristicas relecantes
# ==============================================================================
target = (
data[['building_id', 'primary_use']]
.drop_duplicates()
.set_index('building_id')
.loc[ts_features.index, :]
['primary_use']
)
assert ts_features.index.equals(target.index)
ts_features_selected = select_features(
X = ts_features,
y = target,
fdr_level = 0.001 # Un filtrado muy estricto
)
ts_features_selected.index.name = 'building_id'
print(f"Número de variables antes de la selección: {ts_features.shape[1]}")
print(f"Número de variables tras la selección: {ts_features_selected.shape[1]}")
ts_features_selected.head()