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 con modelos fundacionales
- 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
Data drift¶
En el contexto del forecasting y el machine learning, el data drift se refiere a un cambio en las propiedades estadísticas de los datos de entrada a lo largo del tiempo respecto a los datos con los que se entrenó originalmente el modelo. Cuando esto ocurre, el modelo puede empezar a generar predicciones menos precisas o poco fiables, ya que deja de generalizar bien a la nueva distribución de los datos.
El data drift puede presentarse de varias formas:
Covariate Drift (Feature Drift): cambia la distribución de las variables de entrada, pero la relación entre estas y la variable respuesta se mantiene. Ejemplo: un modelo se entrenó cuando una variable tomaba valores en un determinado rango. Si con el tiempo esa variable pasa a tomar valores en un rango distinto, se produce covariate drift.
Prior Probability Drift (Label Drift): cambia la distribución de la variable respuesta. Ejemplo: un modelo entrenado para predecir el consumo energético durante una estación puede fallar si los patrones estacionales cambian debido a factores externos.
Concept Drift: cambia la relación entre las variables de entrada y la variable respuesta. Ejemplo: un modelo que predice el consumo energético a partir de datos meteorológicos puede fallar si nuevas tecnologías o comportamientos alteran la forma en que el clima afecta al consumo.
Detectar y gestionar el data drift es fundamental para mantener la fiabilidad de los modelos en producción. Algunas estrategias habituales son:
Monitorizar los datos de entrada durante la predicción para detectar cambios cuanto antes.
Hacer seguimiento de las métricas de rendimiento del modelo (por ejemplo, MAE, RMSE) a lo largo del tiempo.
Reentrenar los modelos periódicamente con datos recientes para adaptarlos a la evolución de las condiciones.
Skforecast incluye dos clases específicas para la detección de data drift:
PopulationDriftDetector: detecta cambios a nivel de población, lo que ayuda a identificar cuándo es necesario reentrenar un modelo de forecasting.RangeDriftDetector: detecta cambios a nivel de observación individual, adecuado para validar los datos de entrada durante la fase de predicción.
Detección de drift durante el entrenamiento del modelo¶
El objetivo último de la detección de drift es responder a una pregunta sencilla pero importante: ¿la distribución de los nuevos datos es distinta de la distribución de los datos de entrenamiento?
Cuando no hay componente temporal y las observaciones son independientes e idénticamente distribuidas (i.i.d.), esta pregunta suele abordarse con tests estadísticos. Estos tests miden algún tipo de distancia entre las distribuciones de los dos conjuntos de datos y calculan un valor de probabilidad (p-value) para determinar si la diferencia es lo suficientemente grande como para indicar un cambio significativo.
Sin embargo, esta aproximación no puede aplicarse directamente a series temporales, cuya distribución cambia de forma natural a lo largo del tiempo debido a patrones como la estacionalidad o la tendencia. Detectar drift en este contexto requiere métodos que tengan en cuenta esta dinámica temporal esperada.
Para ilustrar mejor este concepto, en el siguiente ejemplo se comparan dos meses de una serie temporal con el conjunto de datos completo. En este caso, los meses evaluados se comportan según lo esperado, por lo que no debería detectarse drift.
# Librerías
# ==============================================================================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from skforecast.datasets import fetch_dataset
from skforecast.plot import set_dark_theme
from skforecast.drift_detection import PopulationDriftDetector
from scipy.stats import ks_2samp
# Datos
# ==============================================================================
datos = fetch_dataset('bike_sharing', verbose=False)
datos = datos[['temp', 'hum']]
display(datos.head())
datos_train = datos.iloc[: 9000].copy()
| temp | hum | |
|---|---|---|
| date_time | ||
| 2011-01-01 00:00:00 | 9.84 | 81.0 |
| 2011-01-01 01:00:00 | 9.02 | 80.0 |
| 2011-01-01 02:00:00 | 9.02 | 80.0 |
| 2011-01-01 03:00:00 | 9.84 | 75.0 |
| 2011-01-01 04:00:00 | 9.84 | 75.0 |
# Gráfico de los datos de referencia y del segmento de dos meses a evaluar
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(8, 3))
inicio_datos_test = '2011-12-01 22:00:00'
fin_datos_test = '2012-01-31 23:00:00'
datos_referencia = datos_train.loc[:, 'temp'].copy()
datos_test = datos_train.loc[inicio_datos_test:fin_datos_test, 'temp'].copy()
datos_referencia.plot(ax=ax, label='Datos de referencia')
datos_test.plot(ax=ax, label='Datos test')
ax.legend();
Se utiliza un test de Kolmogorov-Smirnov para comparar la distribución del segmento de dos meses seleccionado con la de la serie de referencia completa. La hipótesis nula establece que ambas muestras proceden de la misma distribución. Si el p-value es inferior a un determinado umbral (habitualmente 0.05), se rechaza la hipótesis nula, lo que indica que las distribuciones son significativamente distintas y sugiere la presencia de drift.
# Test de Kolmogorov-Smirnov para comparar ambos conjuntos de datos
# ==============================================================================
ks_2samp(datos_referencia, datos_test)
KstestResult(statistic=np.float64(0.4571829521829522), pvalue=np.float64(8.54562775188432e-166), statistic_location=np.float64(18.86), statistic_sign=np.int8(-1))
# Gráficos para comparar ambos conjuntos de datos
# ==============================================================================
fig, axs = plt.subplots(ncols=2, figsize=(9, 3))
sns.kdeplot(datos_referencia, label='Datos de referencia', color='#30a2da', ax=axs[0])
sns.kdeplot(datos_test, label='Datos test', color='red', ax=axs[0])
axs[0].set_title('Comparación de distribuciones')
axs[0].set_ylabel('Densidad')
axs[0].legend()
sns.ecdfplot(datos_referencia, label='Datos de referencia', color='#30a2da', ax=axs[1])
sns.ecdfplot(datos_test, label='Datos test', color='red', ax=axs[1])
axs[1].set_title('Comparación de distribuciones acumuladas')
axs[1].set_ylabel('Probabilidad acumulada')
axs[1].legend();
Tanto el test estadístico como las visualizaciones anteriores muestran una clara diferencia entre las distribuciones, aunque se sabe que no existe drift. Esto pone de manifiesto la importancia de utilizar métodos diseñados específicamente para series temporales a la hora de detectar drift, ya que los tests estadísticos tradicionales pueden no ser adecuados debido a las dependencias temporales y los patrones inherentes a este tipo de datos.
Marco basado en distancias para la detección de drift temporal¶
Este marco implementa una aproximación basada en distancias y guiada por los datos para detectar drift temporal (cambios en la distribución subyacente de los datos a lo largo del tiempo) en series temporales. Construye una referencia empírica del comportamiento normal a partir de datos históricos (de referencia) y la utiliza para evaluar si los nuevos datos observados se desvían significativamente de la norma establecida.
La aproximación es independiente tanto del modelo como de la distancia: puede emplearse cualquier distancia o divergencia estadística que cuantifique la disimilitud entre muestras de datos (por ejemplo, Kolmogorov-Smirnov, Chi-cuadrado, divergencia de Jensen-Shannon u otras métricas adecuadas).
1. Fase de referencia: estimación de la distribución empírica¶
El primer paso consiste en caracterizar la variabilidad natural de la serie temporal en condiciones estables.
Seleccionar una ventana de referencia Se elige un segmento histórico de la serie temporal que represente un comportamiento estable y libre de drift. Este segmento actúa como conjunto de datos de referencia.
Segmentar los datos de referencia Se divide la serie temporal de referencia en chunks de igual longitud y sin solapamiento: $$ \{ C_1, C_2, \ldots, C_n \} $$ Cada chunk $C_i$ corresponde a una ventana temporal fija (por ejemplo, una semana, un mes o un número fijo de observaciones).
Calcular las distancias Para cada chunk $C_i$, se calcula su distancia respecto al resto del conjunto de referencia (o a una agregación representativa del mismo). Con ello se obtiene un conjunto de distancias: $$ \mathbb{D}_{ref} = \{ d_1, d_2, \ldots, d_n \} $$
Construir la distribución empírica El conjunto $\mathbb{D}_{ref}$ representa la distribución de las distancias en condiciones normales (sin drift). Cuantifica el nivel típico de disimilitud entre segmentos de datos estables.
Definir un umbral de drift Se selecciona un cuantil (por ejemplo, el percentil 95) de la distribución empírica $\mathbb{D}_{ref}$ como umbral de drift: $$ \tau = Q_{0.95}(\mathbb{D}_{ref}) $$ Cualquier distancia superior a $\tau$ indica una desviación mayor de la esperada bajo la variabilidad normal.
Detección de drift poblacional - Animación
2. Fase de monitorización: detección de drift en nuevos datos¶
Una vez establecida la distribución de referencia, los nuevos datos pueden evaluarse de forma continua para detectar drift.
Dividir los nuevos datos en chunks A medida que se dispone de nuevas observaciones, se segmentan en chunks de la misma longitud que la utilizada en la fase de referencia: $$ \{ C'_1, C'_2, \ldots, C'_m \} $$
Calcular las distancias respecto a la referencia Para cada nuevo chunk $C'_j$, se calcula su distancia respecto a la referencia (ya sea frente a todos los chunks de referencia o frente a una representación agregada de la distribución de referencia).
Comparar con el umbral
- Si $d(C'_j, \text{referencia}) \leq \tau$, los datos son coherentes con la distribución de referencia.
- Si $d(C'_j, \text{referencia}) > \tau$, el chunk se marca como posible drift.
Interpretación Un chunk marcado indica que el nuevo segmento de datos difiere significativamente de la norma histórica, lo que apunta a un posible drift poblacional o cambio de concepto. Estos casos pueden requerir un análisis más detallado, el reentrenamiento del modelo o ajustes en el pipeline de datos.
PopulationDriftDetector¶
La clase PopulationDriftDetector está diseñada para detectar feature drift y label drift en series temporales. Evalúa si la distribución de las variables de entrada (tanto la variable respuesta como las exógenas) se mantiene coherente con la de los datos utilizados para entrenar el modelo de forecasting.
Comparando las observaciones recientes con los datos de entrenamiento, el detector identifica cambios significativos en la distribución que pueden indicar que el modelo necesita reentrenarse.
Las métricas estadísticas utilizadas dependen del tipo de dato:
Variables numéricas: estadístico de Kolmogorov-Smirnov y distancia de Jensen-Shannon.
Variables categóricas: estadístico Chi-cuadrado y distancia de Jensen-Shannon.
La API sigue los mismos principios de diseño que los forecasters de Skforecast:
Los mismos datos utilizados para entrenar un forecaster pueden emplearse para ajustar un
PopulationDriftDetector.Cuando se dispone de nuevos datos históricos (es decir, varias observaciones nuevas), puede utilizarse el método
predictpara comprobar si existe drift.Si se detecta drift, conviene analizar su causa y valorar si es necesario reentrenar o recalibrar el modelo de forecasting.
✏️ Nota
Esta implementación está inspirada en la detección univariante de drift de NannyML, pero ofrece una adaptación ligera ajustada al contexto de series temporales de Skforecast.
- Eficiente en memoria: el detector no almacena los datos de referencia completos. En su lugar, guarda únicamente los estadísticos precalculados necesarios para evaluar el drift de forma eficiente durante la predicción.
- Umbrales empíricos: además de los umbrales basados en la desviación estándar, los umbrales pueden obtenerse a partir del cuantil indicado de las distribuciones empíricas calculadas con los chunks de referencia, utilizando leave-one-chunk-out para evitar el sesgo de compararse consigo mismo.
- Detección de valores fuera de rango: también comprueba si hay valores fuera de rango en las variables numéricas y categorías no vistas en las variables categóricas.
- Soporte para múltiples series temporales: puede trabajar con múltiples series temporales, cada una con sus propias variables exógenas.
Si se necesitan funcionalidades más avanzadas, como la detección de drift multivariante o controles de calidad de los datos, se recomienda utilizar directamente NannyML.
Para ilustrar el funcionamiento de la detección de drift, el conjunto de datos se divide en un conjunto de entrenamiento y una partición de datos nuevos, simulando un escenario real en el que se dispone de datos adicionales después de haber entrenado el modelo.
Para emular el data drift, se modifica intencionadamente la variable temp en la partición de datos nuevos:
Junio: las temperaturas se incrementan en +10 °C.
Julio: las temperaturas se incrementan en +20 °C.
Octubre: las temperaturas se sustituyen por un valor constante igual a la media de los datos originales. Aunque este valor se encuentra dentro del rango original, su falta de variabilidad lo hace estadísticamente atípico.
Diciembre: las temperaturas se reducen en 10 °C.
La variable hum se mantiene sin cambios en toda la partición de datos nuevos y actúa como variable de control para demostrar que el detector no identifica drift cuando no existe.
# Datos
# ==============================================================================
datos = fetch_dataset('bike_sharing', verbose=False)
datos = datos[['temp', 'hum']]
datos_train = datos.iloc[: 9000].copy()
datos_nuevos = datos.iloc[9000:].copy()
# Introducir cambios en la distribución
# ==============================================================================
datos_nuevos_drift = datos_nuevos.copy()
# Sumar 10 a las observaciones de junio de 2012
datos_nuevos_drift.loc['2012-06-01 00:00:00':'2012-06-30 23:00:00', 'temp'] = (
datos_nuevos_drift.loc['2012-06-01 00:00:00':'2012-06-30 23:00:00', 'temp'] + 10
)
# Sumar 20 a las observaciones de julio de 2012
datos_nuevos_drift.loc['2012-07-01 00:00:00':'2012-07-31 23:00:00', 'temp'] = (
datos_nuevos_drift.loc['2012-07-01 00:00:00':'2012-07-31 23:00:00', 'temp'] + 20
)
# Valor medio constante en octubre de 2012
datos_nuevos_drift.loc['2012-10-01 00:00:00':'2012-10-31 23:00:00', 'temp'] = (
datos_nuevos_drift.loc['2012-10-01 00:00:00':'2012-10-31 23:00:00', 'temp'].mean()
)
# Restar 10 a las observaciones de diciembre de 2012
datos_nuevos_drift.loc['2012-12-01 00:00:00':'2012-12-31 23:00:00', 'temp'] = (
datos_nuevos_drift.loc['2012-12-01 00:00:00':'2012-12-31 23:00:00', 'temp'] - 10
)
# Gráfico
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(8, 4))
datos_train.loc[:, 'temp'].plot(ax=ax, label='Train')
datos_nuevos_drift.loc[:, 'temp'].plot(
ax=ax, label='Datos nuevos con drift', color='red'
)
datos_nuevos.loc[:, 'temp'].plot(ax=ax, label='Datos nuevos', color='green')
ax.axhline(
datos_train['temp'].max(), color='white', linestyle=':', label='Máximo train'
)
ax.axhline(
datos_train['temp'].min(), color='white', linestyle=':', label='Mínimo train'
)
ax.legend();
Al crear una instancia de PopulationDriftDetector, deben especificarse cuatro argumentos clave:
chunk_size: define el número de observaciones de cada chunk de datos utilizado para comparar distribuciones. Un tamaño de chunk menor permite comprobar el drift con más frecuencia, pero puede aumentar los falsos positivos debido a una mayor variabilidad. Por el contrario, un tamaño de chunk mayor suaviza la variabilidad, pero puede retrasar la detección del drift. El valor óptimo depende del equilibrio entre sensibilidad y estabilidad que requiera cada aplicación y conjunto de datos.threshold: especifica el umbral utilizado para determinar si se ha producido drift. Cuanto mayor es el umbral, más conservador es el detector a la hora de señalar drift. Su interpretación depende dethreshold_method: con'std'es el número de desviaciones estándar por encima de la media; con'quantile'es el nivel del cuantil (por ejemplo, 0.95) de la distribución empírica.threshold_method: determina cómo se calcula el umbral a partir de la distribución empírica. Las opciones son:'std'para umbrales basados en la desviación estándar. Los umbrales se calculan como media + threshold * desviación estándar. Este método es computacionalmente más rápido, ya que no requiere leave-one-chunk-out.'quantile'para umbrales basados en cuantiles. Los umbrales se obtienen a partir del cuantil indicado de la distribución empírica, utilizando leave-one-chunk-out para evitar el sesgo de compararse consigo mismo.
max_out_of_range_proportion: proporción máxima permitida de observaciones fuera del rango de valores de referencia en las variables numéricas. Si la proporción de valores fuera de rango en un nuevo chunk supera este umbral, se señala drift en la variable correspondiente. Este parámetro proporciona una protección adicional frente a cambios en la distribución que se manifiestan como valores fuera del rango observado en los datos de referencia.
# Ajustar el detector con los datos de entrenamiento
# ==============================================================================
detector = PopulationDriftDetector(
chunk_size = 'MS', # Chunks mensuales
threshold = 3,
threshold_method = 'std',
max_out_of_range_proportion = 0.1
)
detector.fit(datos_train)
detector
PopulationDriftDetector
General Information
- Chunk size: MS
- Threshold: 3
- Threshold method: std
- Max out of range proportion: 0.1
- Is fitted: True
- Skforecast version: 0.25.0
Fitted features
-
['temp', 'hum']
Una vez ajustado, el detector puede utilizarse para evaluar nuevos datos mediante el método predict. Este método devuelve dos DataFrames:
Resultados detallados: contienen información sobre los estadísticos calculados, los umbrales y el estado de drift de cada chunk de datos.
Resumen de resultados: ofrece una visión general con el número y el porcentaje de chunks en los que se ha detectado drift.
# Detectar drift en los nuevos datos
# ==============================================================================
resultados_drift, resumen_drift = detector.predict(datos_nuevos_drift)
# Resultados detallados del drift
# ==============================================================================
resultados_drift
| chunk | chunk_start | chunk_end | feature | ks_statistic | ks_threshold | chi2_statistic | chi2_threshold | js_statistic | js_threshold | reference_range | prop_out_of_range | max_out_of_range_proportion | is_out_of_range | drift_ks_statistic | drift_chi2_statistic | drift_js | drift_detected | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 2012-01-11 | 2012-01-31 23:00:00 | temp | 0.490175 | 0.904773 | NaN | NaN | 0.546958 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 1 | 1 | 2012-02-01 | 2012-02-29 23:00:00 | temp | 0.477663 | 0.904773 | NaN | NaN | 0.523748 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 2 | 2 | 2012-03-01 | 2012-03-31 23:00:00 | temp | 0.232412 | 0.904773 | NaN | NaN | 0.373938 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 3 | 3 | 2012-04-01 | 2012-04-30 23:00:00 | temp | 0.217000 | 0.904773 | NaN | NaN | 0.455947 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 4 | 4 | 2012-05-01 | 2012-05-31 23:00:00 | temp | 0.443082 | 0.904773 | NaN | NaN | 0.539446 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 5 | 5 | 2012-06-01 | 2012-06-30 23:00:00 | temp | 0.902111 | 0.904773 | NaN | NaN | 0.877304 | 0.806410 | (0.8200000000000001, 39.36) | 0.344444 | 0.1 | True | False | False | True | True |
| 6 | 6 | 2012-07-01 | 2012-07-31 23:00:00 | temp | 1.000000 | 0.904773 | NaN | NaN | 1.000000 | 0.806410 | (0.8200000000000001, 39.36) | 1.000000 | 0.1 | True | True | False | True | True |
| 7 | 7 | 2012-08-01 | 2012-08-31 23:00:00 | temp | 0.637269 | 0.904773 | NaN | NaN | 0.652528 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 8 | 8 | 2012-09-01 | 2012-09-30 23:00:00 | temp | 0.446389 | 0.904773 | NaN | NaN | 0.518331 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 9 | 9 | 2012-10-01 | 2012-10-31 23:00:00 | temp | 0.537556 | 0.904773 | NaN | NaN | 0.863793 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | True | True |
| 10 | 10 | 2012-11-01 | 2012-11-30 23:00:00 | temp | 0.468611 | 0.904773 | NaN | NaN | 0.562611 | 0.806410 | (0.8200000000000001, 39.36) | 0.000000 | 0.1 | False | False | False | False | False |
| 11 | 11 | 2012-12-01 | 2012-12-31 23:00:00 | temp | 0.860731 | 0.904773 | NaN | NaN | 0.843966 | 0.806410 | (0.8200000000000001, 39.36) | 0.358871 | 0.1 | True | False | False | True | True |
| 12 | 0 | 2012-01-11 | 2012-01-31 23:00:00 | hum | 0.130825 | 0.385918 | NaN | NaN | 0.152290 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 13 | 1 | 2012-02-01 | 2012-02-29 23:00:00 | hum | 0.161425 | 0.385918 | NaN | NaN | 0.199334 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 14 | 2 | 2012-03-01 | 2012-03-31 23:00:00 | hum | 0.119387 | 0.385918 | NaN | NaN | 0.150733 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 15 | 3 | 2012-04-01 | 2012-04-30 23:00:00 | hum | 0.278944 | 0.385918 | NaN | NaN | 0.328472 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 16 | 4 | 2012-05-01 | 2012-05-31 23:00:00 | hum | 0.093703 | 0.385918 | NaN | NaN | 0.205141 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 17 | 5 | 2012-06-01 | 2012-06-30 23:00:00 | hum | 0.171722 | 0.385918 | NaN | NaN | 0.240059 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 18 | 6 | 2012-07-01 | 2012-07-31 23:00:00 | hum | 0.103219 | 0.385918 | NaN | NaN | 0.178075 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 19 | 7 | 2012-08-01 | 2012-08-31 23:00:00 | hum | 0.110520 | 0.385918 | NaN | NaN | 0.196713 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 20 | 8 | 2012-09-01 | 2012-09-30 23:00:00 | hum | 0.076111 | 0.385918 | NaN | NaN | 0.196889 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 21 | 9 | 2012-10-01 | 2012-10-31 23:00:00 | hum | 0.125477 | 0.385918 | NaN | NaN | 0.217908 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 22 | 10 | 2012-11-01 | 2012-11-30 23:00:00 | hum | 0.217556 | 0.385918 | NaN | NaN | 0.280111 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
| 23 | 11 | 2012-12-01 | 2012-12-31 23:00:00 | hum | 0.096502 | 0.385918 | NaN | NaN | 0.187856 | 0.423199 | (0.0, 100.0) | 0.000000 | 0.1 | False | False | False | False | False |
# Resumen del drift
# ==============================================================================
resumen_drift
| feature | n_chunks_with_drift | pct_chunks_with_drift | chunks_with_drift | |
|---|---|---|---|---|
| 0 | hum | 0 | 0.000000 | [] |
| 1 | temp | 4 | 33.333333 | [5, 6, 9, 11] |
Tal y como se esperaba, el detector identifica drift en los datos nuevos modificados, mientras que no detecta drift en los datos sin alterar. Los chunks 5, 6, 9 y 11 corresponden a junio, julio, octubre y diciembre de 2012, los cuatro meses en los que se modificaron los valores de temp. La variable de control hum no muestra drift, como era de esperar.
Las columnas de resultados_drift muestran qué criterio ha provocado cada detección (un chunk se marca si al menos uno de ellos supera su umbral):
Julio (+20 °C) es el único chunk marcado por los tres criterios: Kolmogorov-Smirnov, Jensen-Shannon y valores fuera de rango.
Junio (+10 °C) y diciembre (-10 °C) se marcan por la distancia de Jensen-Shannon y por la proporción de valores fuera de rango (en torno al 34% y al 36%, muy por encima del 10% permitido), mientras que su estadístico de Kolmogorov-Smirnov se queda justo por debajo del umbral.
Octubre (valor constante) solo se marca por la distancia de Jensen-Shannon. El valor constante se encuentra dentro del rango de entrenamiento, por lo que la comprobación de valores fuera de rango no puede detectarlo, pero la concentración de la distribución en un único valor sí queda recogida por la distancia entre histogramas.
Esto muestra por qué combinar varios criterios complementarios hace que el detector sea más robusto que si se basara en un único estadístico.
# Resaltar los chunks con drift detectado
# ==============================================================================
set_dark_theme()
fig, ax = plt.subplots(figsize=(8, 4))
datos_train.loc[:, 'temp'].plot(ax=ax, label='Train')
datos_nuevos_drift.loc[:, 'temp'].plot(
ax=ax, label='Datos nuevos con drift', color='red'
)
datos_nuevos.loc[:, 'temp'].plot(ax=ax, label='Datos nuevos', color='green')
ax.axhline(
datos_train['temp'].max(), color='white', linestyle=':', label='Máximo train'
)
ax.axhline(
datos_train['temp'].min(), color='white', linestyle=':', label='Mínimo train'
)
for row in resultados_drift.query('drift_detected == True').itertuples():
ax.axvspan(
row.chunk_start, row.chunk_end, color='red', alpha=0.3,
label='Drift detectado'
)
# Eliminar etiquetas repetidas en la leyenda
handles, labels = ax.get_legend_handles_labels()
by_label = dict(zip(labels, handles))
ax.legend(by_label.values(), by_label.keys());
PopulationDriftDetector puede utilizarse con múltiples series temporales de forma simultánea, cada una con sus propias variables. En este caso, los datos de entrada deben ser un DataFrame de pandas con un MultiIndex, en el que el primer nivel es el identificador de la serie y el segundo corresponde al índice temporal.
# Datos multiserie
# ==============================================================================
datos_multiserie = pd.concat(
[
datos.assign(series='series_1'),
datos.assign(series='series_2'),
datos.assign(series='series_3')
]
).set_index('series', append=True).swaplevel(0, 1)
display(datos_multiserie)
# División train/nuevos datos para cada serie
datos_multiserie_train = (
datos_multiserie
.groupby(level='series', group_keys=False)
.apply(lambda x: x.iloc[:9000])
)
datos_multiserie_nuevos = (
datos_multiserie
.groupby(level='series', group_keys=False)
.apply(lambda x: x.iloc[9000:])
)
| temp | hum | ||
|---|---|---|---|
| series | date_time | ||
| series_1 | 2011-01-01 00:00:00 | 9.84 | 81.0 |
| 2011-01-01 01:00:00 | 9.02 | 80.0 | |
| 2011-01-01 02:00:00 | 9.02 | 80.0 | |
| 2011-01-01 03:00:00 | 9.84 | 75.0 | |
| 2011-01-01 04:00:00 | 9.84 | 75.0 | |
| ... | ... | ... | ... |
| series_3 | 2012-12-31 19:00:00 | 10.66 | 60.0 |
| 2012-12-31 20:00:00 | 10.66 | 60.0 | |
| 2012-12-31 21:00:00 | 10.66 | 60.0 | |
| 2012-12-31 22:00:00 | 10.66 | 56.0 | |
| 2012-12-31 23:00:00 | 10.66 | 65.0 |
52632 rows × 2 columns
# PopulationDriftDetector multiserie
# ==============================================================================
detector = PopulationDriftDetector(
chunk_size = 'MS', # Chunks mensuales
threshold = 3,
threshold_method = 'std',
max_out_of_range_proportion = 0.1
)
detector.fit(datos_multiserie_train)
detector
PopulationDriftDetector
General Information
- Chunk size: MS
- Threshold: 3
- Threshold method: std
- Max out of range proportion: 0.1
- Is fitted: True
- Skforecast version: 0.25.0
Fitted features
-
{'series_1': ['temp', 'hum'], 'series_2': ['temp', 'hum'], 'series_3': ['temp', 'hum']}
# Umbrales de cada serie
# ==============================================================================
detector.get_thresholds()
| series_id | feature | ks_threshold | chi2_threshold | js_threshold | max_out_of_range_proportion | |
|---|---|---|---|---|---|---|
| 0 | series_1 | temp | 0.904773 | NaN | 0.806410 | 0.1 |
| 1 | series_1 | hum | 0.385918 | NaN | 0.423199 | 0.1 |
| 2 | series_2 | temp | 0.904773 | NaN | 0.806410 | 0.1 |
| 3 | series_2 | hum | 0.385918 | NaN | 0.423199 | 0.1 |
| 4 | series_3 | temp | 0.904773 | NaN | 0.806410 | 0.1 |
| 5 | series_3 | hum | 0.385918 | NaN | 0.423199 | 0.1 |
# Detectar drift en los nuevos datos multiserie
# ==============================================================================
resultados_drift, resumen_drift = detector.predict(datos_multiserie_nuevos)
resumen_drift
| series_id | feature | n_chunks_with_drift | pct_chunks_with_drift | chunks_with_drift | |
|---|---|---|---|---|---|
| 0 | series_1 | hum | 0 | 0.0 | [] |
| 1 | series_1 | temp | 0 | 0.0 | [] |
| 2 | series_2 | hum | 0 | 0.0 | [] |
| 3 | series_2 | temp | 0 | 0.0 | [] |
| 4 | series_3 | hum | 0 | 0.0 | [] |
| 5 | series_3 | temp | 0 | 0.0 | [] |
Como los nuevos datos multiserie no se han modificado, no se detecta drift en ninguna de las series.
Detección de drift durante la predicción¶
Skforecast proporciona la clase RangeDriftDetector para detectar covariate drift tanto en una única serie temporal como en múltiples series, así como en las variables exógenas.
El detector comprueba si los datos de entrada (lags y variables exógenas) utilizados para predecir nuevos valores se encuentran dentro del rango de los datos utilizados para entrenar el modelo.
Su API sigue el mismo diseño que los forecasters:
Los datos utilizados para entrenar un forecaster pueden emplearse también para ajustar el
RangeDriftDetector.Los datos que se pasan al método
predictdel forecaster pueden pasarse también al métodopredictdelRangeDriftDetectorpara comprobar si hay drift en los datos de entrada antes de generar las predicciones.Si se detecta drift, conviene analizar su causa y valorar si el modelo sigue siendo adecuado para predecir con los nuevos datos.
# Librerías
# ==============================================================================
from sklearn.ensemble import HistGradientBoostingRegressor
from skforecast.recursive import ForecasterRecursive
from skforecast.drift_detection import RangeDriftDetector
Detección de valores fuera de rango en una única serie¶
El RangeDriftDetector comprueba si los valores de una serie temporal siguen siendo coherentes con los datos vistos durante el entrenamiento.
En las variables numéricas, verifica que cada nuevo valor se encuentre dentro del rango entre el mínimo y el máximo de los datos de entrenamiento. Los valores fuera de este rango se señalan como posible drift.
En las variables categóricas, comprueba si cada nueva categoría se observó durante el entrenamiento. Las categorías no vistas se señalan como posible drift.
Este mecanismo permite identificar rápidamente cuándo el modelo recibe datos de entrada distintos de aquellos con los que se entrenó, lo que ayuda a decidir si conviene reentrenar el modelo o ajustar el preprocesado.
# Datos simulados
# ==============================================================================
rng = np.random.default_rng(123)
y_train = pd.Series(
rng.normal(loc=10, scale=2, size=100),
index=pd.date_range(start='2020-01-01', periods=100),
name='y',
)
exog_train = pd.DataFrame(
{
'exog_1': rng.normal(loc=10, scale=2, size=100),
'exog_2': rng.choice(['A', 'B', 'C', 'D', 'E'], size=100),
},
index=y_train.index,
)
display(y_train.head())
display(exog_train.head())
2020-01-01 8.021757 2020-01-02 9.264427 2020-01-03 12.575851 2020-01-04 10.387949 2020-01-05 11.840462 Freq: D, Name: y, dtype: float64
| exog_1 | exog_2 | |
|---|---|---|
| 2020-01-01 | 8.968465 | B |
| 2020-01-02 | 13.316227 | B |
| 2020-01-03 | 9.405475 | A |
| 2020-01-04 | 7.233246 | A |
| 2020-01-05 | 9.437591 | A |
# Entrenar RangeDriftDetector
# ==============================================================================
detector = RangeDriftDetector()
detector.fit(y=y_train, exog=exog_train)
detector
RangeDriftDetector
General Information
- Fitted series: y
- Fitted exogenous: exog_1, exog_2
- Series-specific exogenous: False
- Is fitted: True
- Skforecast version: 0.25.0
Series value ranges
-
{'y': (5.5850578036003915, 14.579819894629157)}
Exogenous value ranges
-
{'exog_1': (4.5430286262543085, 14.531041199734418), 'exog_2': {'E', 'B', 'D', 'C', 'A'}}
Supóngase que el modelo está desplegado en producción y que se utilizan nuevos datos para predecir valores futuros. Se simula un covariate drift en la serie objetivo y en las variables exógenas para ilustrar cómo utilizar la clase RangeDriftDetector para detectarlo.
# Predicción con datos con drift
# ==============================================================================
last_window = pd.Series(
[6.6, 7.5, 100, 9.3, 10.2], name='y' # El valor 100 está fuera de rango
)
exog_predict = pd.DataFrame(
{
'exog_1': [8, 9, 10, 70, 12], # El valor 70 está fuera de rango
'exog_2': ['A', 'B', 'C', 'D', 'W'], # El valor 'W' está fuera de rango
}
)
flag_out_of_range, series_out_of_range, exog_out_of_range = detector.predict(
last_window = last_window,
exog = exog_predict,
verbose = True,
suppress_warnings = False
)
print('Fuera de rango detectado :', flag_out_of_range)
print('Series fuera de rango :', series_out_of_range)
print('Exógenas fuera de rango :', exog_out_of_range)
╭────────────────────────────── FeatureOutOfRangeWarning ──────────────────────────────╮ │ 'y' has values outside the range seen during training [5.58506, 14.57982]. This may │ │ affect the accuracy of the predictions. │ │ │ │ Category : skforecast.exceptions.FeatureOutOfRangeWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\dri │ │ ft_detection\_range_drift.py:284 │ │ Suppress : warnings.simplefilter('ignore', category=FeatureOutOfRangeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────── FeatureOutOfRangeWarning ──────────────────────────────╮ │ 'exog_1' has values outside the range seen during training [4.54303, 14.53104]. This │ │ may affect the accuracy of the predictions. │ │ │ │ Category : skforecast.exceptions.FeatureOutOfRangeWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\dri │ │ ft_detection\_range_drift.py:284 │ │ Suppress : warnings.simplefilter('ignore', category=FeatureOutOfRangeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────── FeatureOutOfRangeWarning ──────────────────────────────╮ │ 'exog_2' has values not seen during training. Seen values: {'E', 'B', 'D', 'C', │ │ 'A'}. This may affect the accuracy of the predictions. │ │ │ │ Category : skforecast.exceptions.FeatureOutOfRangeWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\dri │ │ ft_detection\_range_drift.py:284 │ │ Suppress : warnings.simplefilter('ignore', category=FeatureOutOfRangeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────── Out-of-range summary ──────────────────────────────╮ │ Series: │ │ 'y' has values outside the observed range [5.58506, 14.57982]. │ │ │ │ Exogenous Variables: │ │ 'exog_1' has values outside the observed range [4.54303, 14.53104]. │ │ 'exog_2' has values not seen during training. Seen values: {'E', 'B', 'D', 'C', │ │ 'A'}. │ ╰─────────────────────────────────────────────────────────────────────────────────╯
Fuera de rango detectado : True Series fuera de rango : ['y'] Exógenas fuera de rango : ['exog_1', 'exog_2']
Detección de valores fuera de rango en múltiples series¶
El mismo proceso se aplica cuando se modelan múltiples series temporales.
Para cada serie, el
RangeDriftDetectorcomprueba si los nuevos valores se mantienen dentro del rango de los datos de entrenamiento.Si se incluyen variables exógenas, estas se comprueban agrupadas por serie, de forma que el drift se detecta en el contexto correcto.
Esto permite monitorizar el drift a nivel de cada serie, lo que facilita detectar problemas en series concretas sin que los resultados agregados lleven a conclusiones erróneas.
# Datos simulados: múltiples series temporales
# ==============================================================================
idx = pd.MultiIndex.from_product(
[
['series_1', 'series_2', 'series_3'],
pd.date_range(start='2020-01-01', periods=3),
],
names=['series_id', 'datetime'],
)
series_train = pd.DataFrame(
{'values': [1, 2, 3, 10, 20, 30, 100, 200, 300]}, index=idx
)
exog_train = pd.DataFrame(
{
'exog_1': [5.0, 6.0, 7.0, 15.0, 25.0, 35.0, 150.0, 250.0, 350.0],
'exog_2': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
},
index=idx,
)
display(series_train)
display(exog_train)
| values | ||
|---|---|---|
| series_id | datetime | |
| series_1 | 2020-01-01 | 1 |
| 2020-01-02 | 2 | |
| 2020-01-03 | 3 | |
| series_2 | 2020-01-01 | 10 |
| 2020-01-02 | 20 | |
| 2020-01-03 | 30 | |
| series_3 | 2020-01-01 | 100 |
| 2020-01-02 | 200 | |
| 2020-01-03 | 300 |
| exog_1 | exog_2 | ||
|---|---|---|---|
| series_id | datetime | ||
| series_1 | 2020-01-01 | 5.0 | A |
| 2020-01-02 | 6.0 | B | |
| 2020-01-03 | 7.0 | C | |
| series_2 | 2020-01-01 | 15.0 | D |
| 2020-01-02 | 25.0 | E | |
| 2020-01-03 | 35.0 | F | |
| series_3 | 2020-01-01 | 150.0 | G |
| 2020-01-02 | 250.0 | H | |
| 2020-01-03 | 350.0 | I |
# Entrenar RangeDriftDetector: múltiples series temporales
# ==============================================================================
detector = RangeDriftDetector()
detector.fit(series=series_train, exog=exog_train)
# Datos para la predicción con drift: múltiples series temporales
# ==============================================================================
last_window = pd.DataFrame(
{
'series_1': np.array([1.5, 2.3]),
'series_2': np.array([100, 20]), # El valor 100 está fuera de rango
'series_3': np.array([110, 200]),
},
index=pd.date_range(start='2020-01-02', periods=2),
)
idx = pd.MultiIndex.from_product(
[
['series_1', 'series_2', 'series_3'],
pd.date_range(start='2020-01-04', periods=2),
],
names=['series_id', 'datetime'],
)
exog_predict = pd.DataFrame(
{
'exog_1': [5.0, 6.1, 10, 70, 220, 290],
'exog_2': ['A', 'B', 'D', 'F', 'W', 'E'],
},
index=idx,
)
display(last_window)
display(exog_predict)
| series_1 | series_2 | series_3 | |
|---|---|---|---|
| 2020-01-02 | 1.5 | 100 | 110 |
| 2020-01-03 | 2.3 | 20 | 200 |
| exog_1 | exog_2 | ||
|---|---|---|---|
| series_id | datetime | ||
| series_1 | 2020-01-04 | 5.0 | A |
| 2020-01-05 | 6.1 | B | |
| series_2 | 2020-01-04 | 10.0 | D |
| 2020-01-05 | 70.0 | F | |
| series_3 | 2020-01-04 | 220.0 | W |
| 2020-01-05 | 290.0 | E |
# Predicción con datos con drift: múltiples series temporales
# ==============================================================================
flag_out_of_range, series_out_of_range, exog_out_of_range = detector.predict(
last_window = last_window,
exog = exog_predict,
verbose = True,
suppress_warnings = False
)
print('Fuera de rango detectado :', flag_out_of_range)
print('Series fuera de rango :', series_out_of_range)
print('Exógenas fuera de rango :', exog_out_of_range)
╭────────────────────────────── FeatureOutOfRangeWarning ──────────────────────────────╮ │ 'series_2' has values outside the range seen during training [10.00000, 30.00000]. │ │ This may affect the accuracy of the predictions. │ │ │ │ Category : skforecast.exceptions.FeatureOutOfRangeWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\dri │ │ ft_detection\_range_drift.py:284 │ │ Suppress : warnings.simplefilter('ignore', category=FeatureOutOfRangeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────── FeatureOutOfRangeWarning ──────────────────────────────╮ │ 'series_2': 'exog_1' has values outside the range seen during training [15.00000, │ │ 35.00000]. This may affect the accuracy of the predictions. │ │ │ │ Category : skforecast.exceptions.FeatureOutOfRangeWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\dri │ │ ft_detection\_range_drift.py:284 │ │ Suppress : warnings.simplefilter('ignore', category=FeatureOutOfRangeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────── FeatureOutOfRangeWarning ──────────────────────────────╮ │ 'series_3': 'exog_2' has values not seen during training. Seen values: {'H', 'I', │ │ 'G'}. This may affect the accuracy of the predictions. │ │ │ │ Category : skforecast.exceptions.FeatureOutOfRangeWarning │ │ Location : │ │ C:\Users\Joaquin\miniconda3\envs\skforecast_24_py13\Lib\site-packages\skforecast\dri │ │ ft_detection\_range_drift.py:284 │ │ Suppress : warnings.simplefilter('ignore', category=FeatureOutOfRangeWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────── Out-of-range summary ──────────────────────────────╮ │ Series: │ │ 'series_2' has values outside the observed range [10.00000, 30.00000]. │ │ │ │ Exogenous Variables: │ │ 'series_2': 'exog_1' has values outside the observed range [15.00000, 35.00000]. │ │ 'series_3': 'exog_2' has values not seen during training. Seen values: {'H', │ │ 'I', 'G'}. │ ╰──────────────────────────────────────────────────────────────────────────────────╯
Fuera de rango detectado : True
Series fuera de rango : ['series_2']
Exógenas fuera de rango : {'series_2': ['exog_1'], 'series_3': ['exog_2']}
Combinar RangeDriftDetector con forecasters¶
Al desplegar un forecaster en producción, es una buena práctica acompañarlo de un detector de drift. De este modo, ambos se entrenan con el mismo conjunto de datos, y el detector de drift puede verificar los datos de entrada antes de que el forecaster genere las predicciones.
# Datos
# ==============================================================================
datos = fetch_dataset(name='h2o_exog', verbose=False)
datos.index.name = 'datetime'
datos.head(3)
| y | exog_1 | exog_2 | |
|---|---|---|---|
| datetime | |||
| 1992-04-01 | 0.379808 | 0.958792 | 1.166029 |
| 1992-05-01 | 0.361801 | 0.951993 | 1.117859 |
| 1992-06-01 | 0.410534 | 0.952955 | 1.067942 |
# Entrenar el forecaster y el RangeDriftDetector
# ==============================================================================
steps = 36
datos_train = datos.iloc[:-steps, :]
datos_test = datos.iloc[-steps:, :]
forecaster = ForecasterRecursive(
estimator = HistGradientBoostingRegressor(random_state=123),
lags = 15
)
detector = RangeDriftDetector()
forecaster.fit(
y = datos_train['y'],
exog = datos_train[['exog_1', 'exog_2']]
)
detector.fit(
series = datos_train['y'],
exog = datos_train[['exog_1', 'exog_2']]
)
Si se utiliza la last_window almacenada en el forecaster, la detección de drift no es necesaria, ya que corresponde a la última ventana de los datos de entrenamiento. Sin embargo, en entornos de producción puede proporcionarse una last_window externa procedente de otro periodo de tiempo. En ese caso, se recomienda aplicar la detección de drift.
En el siguiente ejemplo, la last_window externa es idéntica a la última ventana de entrenamiento, por lo que no se detectará drift.
# Última ventana (igual que forecaster.last_window_)
# ==============================================================================
last_window = datos_train['y'].iloc[-forecaster.max_lag:]
last_window
datetime 2004-04-01 0.739986 2004-05-01 0.795129 2004-06-01 0.856803 2004-07-01 1.001593 2004-08-01 0.994864 2004-09-01 1.134432 2004-10-01 1.181011 2004-11-01 1.216037 2004-12-01 1.257238 2005-01-01 1.170690 2005-02-01 0.597639 2005-03-01 0.652590 2005-04-01 0.670505 2005-05-01 0.695248 2005-06-01 0.842263 Freq: MS, Name: y, dtype: float64
# Comprobar los datos con RangeDriftDetector y predecir con el forecaster
# ==============================================================================
detector.predict(
last_window = last_window,
exog = datos_test[['exog_1', 'exog_2']],
verbose = True,
suppress_warnings = False
)
predicciones = forecaster.predict(
steps = steps,
last_window = last_window,
exog = datos_test[['exog_1', 'exog_2']]
)
╭───────────────── Out-of-range summary ─────────────────╮ │ Series: │ │ No series with out-of-range values found. │ │ │ │ Exogenous Variables: │ │ No exogenous variables with out-of-range values found. │ ╰────────────────────────────────────────────────────────╯
Información de sesión¶
import session_info
session_info.show(html=False)
----- matplotlib 3.10.9 numpy 2.4.6 pandas 2.3.3 scipy 1.18.0 seaborn 0.13.2 session_info v1.0.1 skforecast 0.25.0 sklearn 1.7.2 ----- IPython 9.15.0 jupyter_client 8.9.1 jupyter_core 5.9.1 ----- Python 3.13.14 | packaged by conda-forge | (main, Jun 12 2026, 09:44:26) [MSC v.1944 64 bit (AMD64)] Windows-11-10.0.26200-SP0 ----- Session information updated at 2026-09-24 16:53
Instrucciones para citar¶
¿Cómo citar este documento?
Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!
Detección de drift en modelos de forecasting de series temporales por Joaquín Amat Rodrigo y Javier Escobar Ortiz, disponible bajo licencia Attribution-NonCommercial-ShareAlike 4.0 International en https://www.cienciadedatos.net/documentos/py70-deteccion-drift-modelos-forecasting.html
¿Cómo citar skforecast?
Si utilizas skforecast, te agradeceríamos mucho que lo cites. ¡Muchas gracias!
Zenodo:
Amat Rodrigo, Joaquin, & Escobar Ortiz, Javier. (2026). skforecast (v0.25.0). Zenodo. https://doi.org/10.5281/zenodo.8382788
APA:
Amat Rodrigo, J., & Escobar Ortiz, J. (2026). skforecast (Version 0.25.0) [Computer software]. https://doi.org/10.5281/zenodo.8382788
BibTeX:
@software{skforecast, author = {Amat Rodrigo, Joaquin and Escobar Ortiz, Javier}, title = {skforecast}, version = {0.25.0}, month = {09}, year = {2026}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382788} }
¿Te ha gustado el artículo? Tu ayuda es importante
Tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊
Este documento creado por Joaquín Amat Rodrigo y Javier Escobar Ortiz tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.
Se permite:
-
Compartir: copiar y redistribuir el material en cualquier medio o formato.
-
Adaptar: remezclar, transformar y crear a partir del material.
Bajo los siguientes términos:
-
Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.
-
No-Comercial: No puedes utilizar el material para fines comerciales.
-
Compartir-Igual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.
