• Introducción
  • Bootstrapped Residuals
    • Librerías y datos
    • Intervalos con residuos in-sample
    • Residuos Out-sample (no condicionados a los valores predichos)
    • Intervalos condicionados a los valores predichos (binned residuals)
    • Predicción de múltiples intervalos
    • Predicción bootstraping, cuantiles y distribución
  • Conformal Prediction
  • Regresión cuantílica
  • Calibración externa de intervalos de predicción
  • Información de sesión
  • Instrucciones para citar


Más sobre forecasting en: cienciadedatos.net


Introducción

Al tratar de anticipar valores futuros de una serie temporal, la mayoría de los modelos de forecasting intentan predecir cuál será el valor más probable, esto se llama point-forecasting. Aunque conocer de antemano el valor esperado de una serie temporal es útil en casi todos los casos de negocio, este tipo de predicción no proporciona información sobre la confianza del modelo ni sobre la incertidumbre de sus predicciones.

El forecasting probabilístico, a diferencia del point-forecasting, es una familia de técnicas que permiten predecir la distribución esperada de la variable respuesta en lugar de un único valor puntual. Este tipo de forecasting proporciona información muy valiosa ya que permite crear intervalos de predicción, es decir, el rango de valores donde es más probable que pueda estar el valor real. Más formalmente, un intervalo de predicción define el intervalo dentro del cual se espera encontrar el verdadero valor de la variable respuesta con una determinada probabilidad.

Skforecast implementa varios métodos para la predicción probabilística:

  • Bootstrapped residuals: El bootstrapping es una técnica estadística que permite estimar la distribución de un estadístico al muestrear repetidamente los datos con reemplazo. En el contexto del forecasting, aplicar bootstrapping a los residuos de un modelo permite estimar la distribución de los errores, lo que facilita la construcción de intervalos de predicción.

  • Conformal prediction: conformal prediction engloba un conjnto de técnicas que permiten generar intervalos de predicción garantizando una determinada cobertura. Se basa en combinar las predicciones puntuales de un modelo de forecasting con sus residuales históricos (diferencias entre predicciones previas y valores observados). Estos residuales permiten estimar la incertidumbre en la predicción y ajustar la amplitud del intervalo entorno a la predicción puntual. Skforecast utiliza Split Conformal Prediction (SCP).

    Además, los métodos conformal pueden calibrar intervalos de predicción generados por otras técnicas, como la regresión cuantílica o bootstrapping. En estos casos, el método conformal ajusta los intervalos para garantizar que sigan siendo válidos con respecto a una determinada de cobertura.

  • Regresión cuantílica (Quantile regression): La regresión cuantílica permite modelar los cuantiles de una variable de respuesta. Al combinar las predicciones de dos modelos de regresión cuantílica, se puede construir un intervalo de predicción en el que cada modelo estima uno de los límites. Por ejemplo, entrenar modelos con Q=0.1 y Q=0.9 produce un intervalo de predicción del 80% (90%10%=80%).

Warning

Tal y como describe Rob J Hyndman en su blog, en los casos reales, casi todos los intervalos de predicción resultan ser demasiado estrechos. Por ejemplo, intervalos teóricos del 95% solo suelen conseguir una cobertura real de entre el 71% y el 87%. Este fenómeno surge debido a que estos intervalos no contemplan todas las fuentes de incertidumbre que, en el caso de modelos de *forecasting*, suelen ser de 4 tipos: el término de error aleatorio, las estimación de parámetros, la elección del modelo y el proceso de predicción de valores futuros. Cuando se calculan intervalos de predicción para modelos de series temporales, generalmente solo se tiene en cuenta el primero de ellos. Por lo tanto, es recomendable utilizar datos de test para validar la cobertura real del intervalo y no confiar únicamente en la esperada.

Bootstrapped Residuals

El error en la predicción del siguiente valor de una serie (one-step-ahead forecast) se define como et=ytˆyt|t1. Asumiendo que los errores futuros serán similares a los errores pasados, es posible simular diferentes predicciones tomando muestras de los errores vistos previamente en el pasado (es decir, los residuos) y agregándolos a las predicciones.

Al hacer esto repetidamente, se crea una colección de predicciones ligeramente diferentes (posibles caminos futuros), que representan la varianza esperada en el proceso de forecasting.

Finalmente, los intervalos de predicción se crean calculando los percentiles α/2 y 1α/2 de los datos simulados en cada horizonte de predicción.


La principal ventaja de esta estrategia es que solo requiere de un único modelo para estimar cualquier intervalo. El inconveniente es la necesidad de ejecutar cientos o miles de iteraciones de bootstrapping lo cual resulta muy costoso desde el punto de vista computacional y no siempre es posible.

Librerías y datos

# Preprocesado de datos
# ==============================================================================
import numpy as np
import pandas as pd
from skforecast.datasets import fetch_dataset

# Plots
# ==============================================================================
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.io as pio
import plotly.offline as poff
pio.templates.default = "seaborn"
pio.renderers.default = 'notebook' 
poff.init_notebook_mode(connected=True)
plt.style.use('seaborn-v0_8-darkgrid')
from skforecast.plot import plot_residuals
from pprint import pprint

# Modelado y Forecasting
# ==============================================================================
import skforecast
from lightgbm import LGBMRegressor
from sklearn.pipeline import make_pipeline
from feature_engine.datetime import DatetimeFeatures
from feature_engine.creation import CyclicalFeatures
from skforecast.recursive import ForecasterRecursive
from skforecast.direct import ForecasterDirect
from skforecast.preprocessing import RollingFeatures
from skforecast.model_selection import TimeSeriesFold, backtesting_forecaster, bayesian_search_forecaster
from skforecast.metrics import calculate_coverage, create_mean_pinball_loss

# Configuración
# ==============================================================================
import warnings
warnings.filterwarnings('once')

color = '\033[1m\033[38;5;208m'
print(f"{color}Version skforecast: {skforecast.__version__}")
Version skforecast: 0.15.0
# Descarga de datos
# ==============================================================================
data = fetch_dataset(name='bike_sharing', raw=False)
data = data[['users', 'temp', 'hum', 'windspeed', 'holiday']]
data = data.loc['2011-04-01 00:00:00':'2012-10-20 23:00:00', :].copy()
data.head(3)
bike_sharing
------------
Hourly usage of the bike share system in the city of Washington D.C. during the
years 2011 and 2012. In addition to the number of users per hour, information
about weather conditions and holidays is available.
Fanaee-T,Hadi. (2013). Bike Sharing Dataset. UCI Machine Learning Repository.
https://doi.org/10.24432/C5W894.
Shape of the dataset: (17544, 11)
users temp hum windspeed holiday
date_time
2011-04-01 00:00:00 6.0 10.66 100.0 11.0014 0.0
2011-04-01 01:00:00 4.0 10.66 100.0 11.0014 0.0
2011-04-01 02:00:00 7.0 10.66 93.0 12.9980 0.0

Se añaden variables adicionales creadas a partir del calendario.

# Variables exógenas basadas en el calendario
# ==============================================================================
features_to_extract = ['month', 'week', 'day_of_week', 'hour']
calendar_transformer = DatetimeFeatures(
                        variables           = 'index',
                        features_to_extract = features_to_extract,
                        drop_original       = False,
                     )

# Codificación de variables cíclicas
# ==============================================================================
features_to_encode = ['month', 'week', 'day_of_week', 'hour']
max_values = {"month": 12, "week": 52, "day_of_week": 7, "hour": 24}
cyclical_encoder = CyclicalFeatures(
                       variables     = features_to_encode,
                       max_values    = max_values,
                       drop_original = True
                   )
exog_transformer = make_pipeline(calendar_transformer, cyclical_encoder)
data = exog_transformer.fit_transform(data)
exog_features = data.columns.difference(['users']).tolist()
data.head(3)
users temp hum windspeed holiday month_sin month_cos week_sin week_cos day_of_week_sin day_of_week_cos hour_sin hour_cos
date_time
2011-04-01 00:00:00 6.0 10.66 100.0 11.0014 0.0 0.866025 -0.5 1.0 6.123234e-17 -0.433884 -0.900969 0.000000 1.000000
2011-04-01 01:00:00 4.0 10.66 100.0 11.0014 0.0 0.866025 -0.5 1.0 6.123234e-17 -0.433884 -0.900969 0.258819 0.965926
2011-04-01 02:00:00 7.0 10.66 93.0 12.9980 0.0 0.866025 -0.5 1.0 6.123234e-17 -0.433884 -0.900969 0.500000 0.866025

Para facilitar el entrenamiento de los modelos, la búsqueda de hiperparámetros óptimos y la evaluación de su capacidad predictiva, los datos se dividen en tres conjuntos separados: entrenamiento, validación y prueba.

# Partición de datos en entrenamiento-validación-test
# ==============================================================================
end_train = '2012-06-30 23:59:00'
end_validation = '2012-10-01 23:59:00'
data_train = data.loc[: end_train, :]
data_val   = data.loc[end_train:end_validation, :]
data_test  = data.loc[end_validation:, :]

print(f"Dates train      : {data_train.index.min()} --- {data_train.index.max()}  (n={len(data_train)})")
print(f"Dates validacion : {data_val.index.min()} --- {data_val.index.max()}  (n={len(data_val)})")
print(f"Dates test       : {data_test.index.min()} --- {data_test.index.max()}  (n={len(data_test)})")
Dates train      : 2011-04-01 00:00:00 --- 2012-06-30 23:00:00  (n=10968)
Dates validacion : 2012-07-01 00:00:00 --- 2012-10-01 23:00:00  (n=2232)
Dates test       : 2012-10-02 00:00:00 --- 2012-10-20 23:00:00  (n=456)
# Gráfico de la serie temporal
# ==============================================================================
fig = go.Figure()
fig.add_trace(go.Scatter(x=data_train.index, y=data_train['users'], mode='lines', name='Train'))
fig.add_trace(go.Scatter(x=data_val.index, y=data_val['users'], mode='lines', name='Validation'))
fig.add_trace(go.Scatter(x=data_test.index, y=data_test['users'], mode='lines', name='Test'))
fig.update_layout(
    title  = 'Número de usuarios',
    xaxis_title="Fecha",
    yaxis_title="Usuarios",
    width=800,
    height=400,
    margin=dict(l=20, r=20, t=35, b=20),
    legend=dict(orientation="h", yanchor="top", y=1, xanchor="left", x=0.001),
)
fig.show()
May 2011Jul 2011Sep 2011Nov 2011Jan 2012Mar 2012May 2012Jul 2012Sep 201202004006008001000
TrainValidationTestNúmero de usuariosFechaUsuarios

Intervalos con residuos in-sample

Los intervalos se pueden calcular utilizando los residuos in-sample (residuos del conjunto de entrenamiento), ya sea llamando al método predict_interval(), o realizando un procedimiento completo de backtesting. Sin embargo, esto puede resultar en intervalos que son demasiado estrechos (demasiado optimistas).

✎ Note

Los hiperparámetros utilizados en este ejemplo han sido previamente optimizados mediante un proceso de búsqueda bayesiana. Para obtener más información sobre este proceso, consulte Hyperparameter tuning and lags selection.

# Crear y entrenar modelo
# ==============================================================================
params = {
    "max_depth": 7,
    "n_estimators": 300,
    "learning_rate": 0.06,
    "verbose": -1,
    "random_state": 15926
}
lags = [1, 2, 3, 23, 24, 25, 167, 168, 169]
window_features = RollingFeatures(stats=["mean"], window_sizes=24 * 3)

forecaster = ForecasterRecursive(
                 regressor       = LGBMRegressor(**params),
                 lags            = lags,
                 window_features = window_features,
             )

forecaster.fit(
    y    = data.loc[:end_validation, 'users'],
    exog = data.loc[:end_validation, exog_features],
    store_in_sample_residuals = True
)
# In-sample residuals stored during fit
# ==============================================================================
print("Amount of residuals stored:", len(forecaster.in_sample_residuals_))
forecaster.in_sample_residuals_
Amount of residuals stored: 10000
array([ 42.02327339,  -4.75342728, -39.26777553, ...,  -3.54886809,
       -41.20842177, -13.42207696])

Se utiliza la función backtesting_forecaster() para generar los intervalos de predicción de todo el conjunto de test. Los principales argumentos de esta función son:

  • use_in_sample_residuals: cuando es True, los residuos in-sample se utilizan para calcular los intervalos de predicción. Dado que estos residuos se obtienen del conjunto de entrenamiento, siempre están disponibles, pero suelen dar lugar a intervalos demasiado optimistas. Cuando es False, los residuos out-sample se utilizan para calcular los intervalos de predicción. Estos residuos se obtienen del conjunto de validación y solo están disponibles si se ha llamado al método set_out_sample_residuals(). Se recomienda utilizar los residuos out-sample para lograr la cobertura deseada.

  • interval: la cobertura deseada de los intervalos de predicción. Por ejemplo, [10, 90] significa que los intervalos de predicción se calculan para los percentiles 10 y 90, lo que da como resultado una cobertura teórica del 80%.

  • interval_method:el método utilizado para calcular los intervalos de predicción. Las opciones disponibles son bootstrapping y conformal.

  • n_boot: el número de muestras bootstrap que se utilizan para estimar los intervalos de predicción cuando interval_method = 'bootstrapping'. Cuanto mayor sea el número de muestras, más precisos serán los intervalos de predicción, pero mayor el tiempo necesario.

# Backtesting con intervalos en los datos de test usando residuos in-sample
# ==============================================================================
cv = TimeSeriesFold(steps = 24, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = [10, 90],  # 80% prediction interval
                          interval_method         = 'bootstrapping',
                          n_boot                  = 150,
                          use_in_sample_residuals = True,  # Use in-sample residuals
                          use_binned_residuals    = False,
                      )
predictions.head(5)
  0%|          | 0/19 [00:00<?, ?it/s]
pred lower_bound upper_bound
2012-10-02 00:00:00 58.387527 32.799491 78.248425
2012-10-02 01:00:00 17.870302 -7.253084 50.190172
2012-10-02 02:00:00 7.901576 -17.057776 37.596425
2012-10-02 03:00:00 5.414332 -16.809510 40.194483
2012-10-02 04:00:00 10.379630 -12.696466 56.904041
# Function to plot predicted intervals
# ======================================================================================
def plot_predicted_intervals(
    predictions: pd.DataFrame,
    y_true: pd.DataFrame,
    target_variable: str,
    initial_x_zoom: list = None,
    title: str = None,
    xaxis_title: str = None,
    yaxis_title: str = None,
):
    """
    Plot predicted intervals vs real values

    Parameters
    ----------
    predictions : pandas DataFrame
        Predicted values and intervals.
    y_true : pandas DataFrame
        Real values of target variable.
    target_variable : str
        Name of target variable.
    initial_x_zoom : list, default `None`
        Initial zoom of x-axis, by default None.
    title : str, default `None`
        Title of the plot, by default None.
    xaxis_title : str, default `None`
        Title of x-axis, by default None.
    yaxis_title : str, default `None`
        Title of y-axis, by default None.
    
    """

    fig = go.Figure([
        go.Scatter(name='Prediction', x=predictions.index, y=predictions['pred'], mode='lines'),
        go.Scatter(name='Real value', x=y_true.index, y=y_true[target_variable], mode='lines'),
        go.Scatter(
            name='Upper Bound', x=predictions.index, y=predictions['upper_bound'],
            mode='lines', marker=dict(color="#444"), line=dict(width=0), showlegend=False
        ),
        go.Scatter(
            name='Lower Bound', x=predictions.index, y=predictions['lower_bound'],
            marker=dict(color="#444"), line=dict(width=0), mode='lines',
            fillcolor='rgba(68, 68, 68, 0.3)', fill='tonexty', showlegend=False
        )
    ])
    fig.update_layout(
        title=title, xaxis_title=xaxis_title, yaxis_title=yaxis_title, width=800,
        height=400, margin=dict(l=20, r=20, t=35, b=20), hovermode="x",
        xaxis=dict(range=initial_x_zoom),
        legend=dict(orientation="h", yanchor="top", y=1.1, xanchor="left", x=0.001)
    )
    fig.show()
# Gráfico intervalos
# ==============================================================================
plot_predicted_intervals(
    predictions     = predictions,
    y_true          = data_test,
    target_variable = "users",
    xaxis_title     = "Date time",
    yaxis_title     = "users",
)

# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = calculate_coverage(
               y_true      = data.loc[end_validation:, 'users'],
               lower_bound = predictions["lower_bound"], 
               upper_bound = predictions["upper_bound"]
           )
print(f"Cobertura del intervalo: {round(100 * coverage, 2)} %")

# Area del intervalo
# ==============================================================================
area = (predictions["upper_bound"] - predictions["lower_bound"]).sum()
print(f"Area of the interval: {round(area, 2)}")
Oct 32012Oct 5Oct 7Oct 9Oct 11Oct 13Oct 15Oct 17Oct 1902004006008001000
Real valuePredictionDate timeusers
Cobertura del intervalo: 61.84 %
Area of the interval: 43177.89

Los intervalos de predicción presentan un exceso de confianza, ya que tienden a ser demasiado estrechos, lo que da lugar a una cobertura real inferior a la cobertura teóritca (80%). Esto se debe a la tendencia de los residuos de entrenamiento (in-sample) a sobrestimar la capacidad de predicción del modelo.

# Almacenar las predicciones para su posterior uso
# ==============================================================================
predictions_in_sample_residuals = predictions.copy()

Residuos Out-sample (no condicionados a los valores predichos)

Para evitar el problema de los intervalos demasiado optimistas, es posible utilizar los residuos out-sample (residuos de un conjunto de validación no vistos durante el entrenamiento). Estos residuos se pueden obtener a través de un proceso de backtesting.

# Backtesting con datos de validación para obtener residuos out-sample
# ==============================================================================
cv = TimeSeriesFold(steps = 24, initial_train_size = len(data.loc[:end_train]))
_, predictions_val = backtesting_forecaster(
                         forecaster    = forecaster,
                         y             = data.loc[:end_validation, 'users'],
                         exog          = data.loc[:end_validation, exog_features],
                         cv            = cv,
                         metric        = 'mean_absolute_error',
                     )
  0%|          | 0/93 [00:00<?, ?it/s]
# Distribución residuos out-sample
# ==============================================================================
residuals = data.loc[predictions_val.index, 'users'] - predictions_val['pred']
print(pd.Series(np.where(residuals < 0, 'negative', 'positive')).value_counts())
plt.rcParams.update({'font.size': 8})
_ = plot_residuals(residuals=residuals, figsize=(7, 4))
positive    1297
negative     935
Name: count, dtype: int64

Con el método set_out_sample_residuals(), los residuos out-sample se almacenan en el objeto forecaster para que puedan ser utilizados para calibrar los intervalos de predicción.

# Almacenar residuos out-sample en el forecaster
# ==============================================================================
forecaster.set_out_sample_residuals(
    y_true = data.loc[predictions_val.index, 'users'], 
    y_pred = predictions_val['pred']
)

Ahora que los nuevos residuos se han añadido al objeto forecaster, los intervalos de predicción se pueden calcular utilizando use_in_sample_residuals = False.

# Backtesting con intervalos en los datos de test usando residuos out-sample
# ==============================================================================
cv = TimeSeriesFold(steps = 24, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = [10, 90],  # 80% prediction interval
                          interval_method         = 'bootstrapping',
                          n_boot                  = 150,
                          use_in_sample_residuals = False,  # Use out-sample residuals
                          use_binned_residuals    = False,
                      )
predictions.head(3)
  0%|          | 0/19 [00:00<?, ?it/s]
pred lower_bound upper_bound
2012-10-02 00:00:00 58.387527 30.961801 133.296285
2012-10-02 01:00:00 17.870302 -14.031592 132.635745
2012-10-02 02:00:00 7.901576 -32.265023 142.360525
# Gráfico intervalos
# ==============================================================================
plot_predicted_intervals(
    predictions     = predictions,
    y_true          = data_test,
    target_variable = "users",
    xaxis_title     = "Date time",
    yaxis_title     = "users",
)

# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = calculate_coverage(
               y_true      = data.loc[end_validation:, 'users'],
               lower_bound = predictions["lower_bound"], 
               upper_bound = predictions["upper_bound"]
           )
print(f"Cobertura del intervalo: {round(100*coverage, 2)} %")

# Area del intervalo
# ==============================================================================
area = (predictions["upper_bound"] - predictions["lower_bound"]).sum()
print(f"Area del intervalo: {round(area, 2)}")
Oct 32012Oct 5Oct 7Oct 9Oct 11Oct 13Oct 15Oct 17Oct 1902004006008001000
Real valuePredictionDate timeusers
Cobertura del intervalo: 83.33 %
Area del intervalo: 101257.22

Los intervalos de predicción obtenidos utilizando los residuos out-sample son considerablemente más amplios que los basados en los residuos in-sample, lo que da como resultado una cobertura empírica más cercana a la cobertura nominal. Sin embargo, al examinar el gráfico, es fácil ver que los intervalos son especialmente amplios cuando los valores predichos son bajos, lo que indica que el modelo no es capaz de localizar correctamente la incertidumbre de sus predicciones.

# Almacenar las predicciones para su posterior uso
# ==============================================================================
predictions_out_sample_residuals = predictions.copy()

Intervalos condicionados a los valores predichos (binned residuals)

El proceso de bootstrapping asume que los residuos se distribuyen de forma independiente por lo que pueden utilizarse sin tener en cuenta del valor predicho. En realidad, esto rara vez es cierto; en la mayoría de los casos, la magnitud de los residuos está correlacionado con la magnitud del valor predicho. En este caso, por ejemplo, difícilmente cabría esperar que el error fuera el mismo cuando el número previsto de usuarios es cercano a cero que cuando es de varios cientos.

Para tener en cuenta esta dependencia, skforecast permite distribuir los residuos en K intervalos, en los que cada intervalo está asociado a un rango de valores predichos. Con esta estrategia, el proceso de bootstrapping muestrea los residuos de diferentes intervalos en función del valor predicho, lo que puede mejorar la cobertura del intervalo y ajustar su anchura cuando sea necesario, permitiendo que el modelo distribuya mejor la incertidumbre de sus predicciones.

Para que el forecaster pueda agrupar los residuos out-sample, los valores predichos se pasan al método set_out_sample_residuals() junto con los residuos. Internamente, skforecast utiliza la clase QuantileBinner para agrupar los datos en intervalos basados en cuantiles utilizando numpy.percentile. Esta clase es similar a KBinsDiscretizer pero más rápida para agrupar datos en intervalos basados en cuantiles. Los intervalos se definen siguiendo la convención: bins[i-1] <= x < bins[i]. El proceso de binning puede ajustarse utilizando el argumento binner_kwargs del objeto Forecaster. Traducción realizada con la versión gratuita del traductor DeepL.com

# Crear forecaster
# ==============================================================================
forecaster = ForecasterRecursive(
                 regressor     = LGBMRegressor(**params),
                 lags          = lags,
                 binner_kwargs = {'n_bins': 5}   
             )

forecaster.fit(
    y    = data.loc[:end_validation, 'users'],
    exog = data.loc[:end_validation, exog_features],
    store_in_sample_residuals = True
)

Durante el proceso de entrenamiento, el forecaster utiliza las predicciones in-sample para definir los intervalos en los que se almacenan los residuos en función del valor predicho al que están relacionados (binner_intervals_). Por ejemplo, si el bin "0" tiene un intervalo de (5.5, 10.7), significa que almacenará los residuos de las predicciones que caigan dentro de ese intervalo.

Cuando se calculan los intervalos de predicción, los residuos se muestrean del bin correspondiente al valor predicho. De esta forma, el modelo puede ajustar la anchura de los intervalos en función del valor predicho, lo que puede ayudar a distribuir mejor la incertidumbre de las predicciones.

# Intervalos de los bins
# ==============================================================================
pprint(forecaster.binner_intervals_)
{0: (-0.8943794883271247, 30.676160623625577),
 1: (30.676160623625577, 120.49652689276296),
 2: (120.49652689276296, 209.69596300954962),
 3: (209.69596300954962, 338.3331511270013),
 4: (338.3331511270013, 955.802117392104)}

El método set_out_sample_residuals() agrupa los residuos según los intervalos aprendidos durante el entrenamiento. Para evitar utilizar demasiada memoria, el número de residuos almacenados por intervalo se limita a 10_000 // self.binner.n_bins_.

# Almacenar residuos out-sample en el forecaster
# ==============================================================================
forecaster.set_out_sample_residuals(
    y_true = data.loc[predictions_val.index, 'users'],
    y_pred = predictions_val['pred']
)
# Número de residuos por bin
# ==============================================================================
for k, v in forecaster.out_sample_residuals_by_bin_.items():
    print(f" Bin {k}: n={len(v)}")
 Bin 0: n=321
 Bin 1: n=315
 Bin 2: n=310
 Bin 3: n=534
 Bin 4: n=752
# Distribución de los residuos por bin
# ==============================================================================
out_sample_residuals_by_bin_df = pd.DataFrame(
    dict([(k, pd.Series(v)) for k, v in forecaster.out_sample_residuals_by_bin_.items()])
)
fig, ax = plt.subplots(figsize=(6, 3))
out_sample_residuals_by_bin_df.boxplot(ax=ax)
ax.set_title("Distribución de los residuos por bin")
ax.set_xlabel("Bin")
ax.set_ylabel("Resoduals");

El gráfico de cajas muestra cómo la dispersión y magnitud de los residuos difieren en función del valor predicho. Los residuos son mayores y más dispersos cuando el valor predicho es mayor (bin más alto), lo que es consistente con la intuición de que los errores tienden a ser mayores cuando el valor predicho es mayor.

# Summary información de los bins
# ======================================================================================
bins_summary = {
    key: pd.DataFrame(value).describe().T.assign(bin=key)
    for key, value
    in forecaster.out_sample_residuals_by_bin_.items()
}
bins_summary = pd.concat(bins_summary.values()).set_index("bin")
bins_summary.insert(0, "interval", bins_summary.index.map(forecaster.binner_intervals_))
bins_summary['interval'] = bins_summary['interval'].apply(lambda x: np.round(x, 2))
bins_summary
interval count mean std min 25% 50% 75% max
bin
0 [-0.89, 30.68] 321.0 1.113833 12.168815 -19.168802 -4.060555 -0.435406 3.392902 150.505840
1 [30.68, 120.5] 315.0 1.560517 31.195637 -77.399525 -13.424717 -3.511859 10.819653 356.446585
2 [120.5, 209.7] 310.0 17.209087 51.763411 -147.240066 -7.478638 15.740146 38.113001 300.170968
3 [209.7, 338.33] 534.0 14.883687 69.231176 -241.322880 -18.266984 10.051186 42.654985 464.619346
4 [338.33, 955.8] 752.0 18.740171 105.830048 -485.278643 -23.429556 25.356507 77.634913 382.818946

Por último, los intervalos de predicción para los datos de test se estiman mediante el proceso de backtesting, con los residuos out-sample condicionados a los valores predichos.

# Backtesting con intervalos en los datos de test usando residuos out-sample
# ==============================================================================
cv = TimeSeriesFold(steps = 24, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = [10, 90],  # 80% prediction interval
                          interval_method         = 'bootstrapping',
                          n_boot                  = 150,
                          use_in_sample_residuals = False,  # Use out-sample residuals
                          use_binned_residuals    = True,   # Residuals conditioned on predicted values
                      )
predictions.head(3)
  0%|          | 0/19 [00:00<?, ?it/s]
pred lower_bound upper_bound
2012-10-02 00:00:00 60.399647 38.496874 92.876947
2012-10-02 01:00:00 17.617464 9.065502 43.844047
2012-10-02 02:00:00 9.002365 -0.636921 26.285576
# Gráfico intervalos
# ==============================================================================
plot_predicted_intervals(
    predictions     = predictions,
    y_true          = data_test,
    target_variable = "users",
    xaxis_title     = "Date time",
    yaxis_title     = "users",
)
# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = calculate_coverage(
               y_true      = data.loc[end_validation:, 'users'],
               lower_bound = predictions["lower_bound"], 
               upper_bound = predictions["upper_bound"]
           )
print(f"Cobertura del intervalo: {round(100*coverage, 2)} %")

# Area del intervalo
# ==============================================================================
area = (predictions["upper_bound"] - predictions["lower_bound"]).sum()
print(f"Area del intervalo: {round(area, 2)}")
Oct 32012Oct 5Oct 7Oct 9Oct 11Oct 13Oct 15Oct 17Oct 1902004006008001000
Real valuePredictionDate timeusers
Cobertura del intervalo: 86.4 %
Area del intervalo: 95072.24

Cuando se utilizan residuos out-sample condicionados al valor predicho, el área del intervalo se reduce significativamente y la incertidumbre se asigna principalmente a las predicciones con valores altos. Sin embargo, la cobertura empírica sigue siendo superior a la cobertura esperada, lo que significa que los intervalos estimados son conservadores.

El siguiente gráfico compara los intervalos de predicción obtenidos utilizando los residuos in-sample, out-sample y out-sample condicionados a los valores predichos.

# Intervalos utilizando: in-sample residuals, out-sample residuals and binned residuals
# ==============================================================================
predictions_out_sample_residuals_binned = predictions.copy()
fig, ax = plt.subplots(figsize=(8, 4))
ax.fill_between(
    data_test.index,
    predictions_out_sample_residuals["lower_bound"],
    predictions_out_sample_residuals["upper_bound"],
    color='gray',
    alpha=0.9,
    label='Out-sample residuals',
    zorder=1
)
ax.fill_between(
    data_test.index,
    predictions_out_sample_residuals_binned["lower_bound"],
    predictions_out_sample_residuals_binned["upper_bound"],
    color='#fc4f30',
    alpha=0.7,
    label='Out-sample binned residuals',
    zorder=2
)

ax.fill_between(
    data_test.index,
    predictions_in_sample_residuals["lower_bound"],
    predictions_in_sample_residuals["upper_bound"],
    color='#30a2da',
    alpha=0.9,
    label='In-sample residuals',
    zorder=3
)
ax.set_xlim(pd.to_datetime(["2012-10-08 00:00:00", "2012-10-15 00:00:00"]))
ax.set_title("Prediction intervals with different residuals", fontsize=12)
ax.legend();

Warning

Forecasting probabilístico en producción La correcta estimación de los intervalos de predicción depende de que los residuos sean representativos de los errores futuros. Por esta razón, se deben utilizar los residuos *out-of-sample*. Sin embargo, la dinámica de las series y los modelos pueden cambiar con el tiempo, por lo que es importante monitorizar y actualizar regularmente los residuos. Esto se puede hacer fácilmente utilizando el método set_out_sample_residuals().

Predicción de múltiples intervalos

La función backtesting_forecaster no solo permite estimar un único intervalo, sino también estimar múltiples cuantiles (percentiles) a partir de los cuales se pueden construir múltiples intervalos de predicción. Esto es útil para evaluar la calidad de los intervalos de predicción para un rango de probabilidades. Además, tiene casi ningún costo computacional adicional en comparación con la estimación de un solo intervalo.

A continuación, se predicen varios percentiles y a partir de estos, se crean intervalos de predicción para diferentes niveles de cobertura nominal - 10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90% y 95% - y se evalúa su cobertura real.

# Intervalos de predicción para varios porcentajes de cobertura
# ==============================================================================
quantiles = [2.5, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 97.5]
intervals = [[2.5, 97.5], [5, 95], [10, 90], [15, 85], [20, 80], [30, 70], [35, 65], [40, 60], [45, 55]]
observed_coverages = []
observed_areas = []

metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = quantiles,
                          interval_method         = 'bootstrapping',
                          n_boot                  = 150,
                          use_in_sample_residuals = False,  # Use out-sample residuals
                          use_binned_residuals    = True
                      )
predictions.head()
  0%|          | 0/19 [00:00<?, ?it/s]
pred p_2.5 p_5 p_10 p_15 p_20 p_25 p_30 p_35 p_40 ... p_55 p_60 p_65 p_70 p_75 p_80 p_85 p_90 p_95 p_97.5
2012-10-02 00:00:00 60.399647 26.540465 32.237144 38.496874 42.515102 46.248204 48.730080 49.747541 50.830767 53.532796 ... 59.877370 61.388407 63.572870 66.179578 70.417796 77.232456 82.866190 92.876947 109.483516 118.113906
2012-10-02 01:00:00 17.617464 4.028027 6.993912 9.065502 10.218569 11.732744 13.290492 15.065701 15.542016 16.594937 ... 19.700862 21.330240 22.453599 25.298972 28.853490 32.110319 35.993433 43.844047 68.261638 77.072027
2012-10-02 02:00:00 9.002365 -5.454406 -2.921308 -0.636921 2.627889 4.072849 5.556213 6.372106 7.170950 8.269832 ... 11.673119 12.670580 14.060076 15.750417 17.660921 19.929581 23.416450 26.285576 41.378533 52.759679
2012-10-02 03:00:00 5.306644 -7.830956 -6.205384 -1.102749 -0.500265 1.101968 2.283863 3.161327 4.006352 4.764298 ... 6.650199 7.561984 8.280507 9.689953 10.880484 11.786155 14.400937 17.854639 24.489084 41.218966
2012-10-02 04:00:00 9.439573 -6.712543 -2.292124 3.206393 5.276201 5.718478 6.576003 7.203672 8.176026 9.025843 ... 11.509308 12.074734 12.427330 13.552448 14.515979 15.536160 17.744162 22.566557 27.379155 53.829421

5 rows × 22 columns

# Calcular cobertura y área de cada intervalo
# ==============================================================================
for interval in intervals:
    observed_coverage = calculate_coverage(
                            y_true      = data.loc[end_validation:, 'users'],
                            lower_bound = predictions[f"p_{interval[0]}"], 
                            upper_bound = predictions[f"p_{interval[1]}"]
                        )
    observed_area = (predictions[f"p_{interval[1]}"] - predictions[f"p_{interval[0]}"]).sum()
    observed_coverages.append(100 * observed_coverage)
    observed_areas.append(observed_area)

results = pd.DataFrame({
              'Interval': intervals,
              'Nominal coverage': [interval[1] - interval[0] for interval in intervals],
              'Observed coverage': observed_coverages,
              'Area': observed_areas
          })
results.round(1)
Interval Nominal coverage Observed coverage Area
0 [2.5, 97.5] 95.0 96.7 163585.0
1 [5, 95] 90.0 93.4 129723.9
2 [10, 90] 80.0 86.4 95072.2
3 [15, 85] 70.0 79.8 73890.3
4 [20, 80] 60.0 74.1 58018.5
5 [30, 70] 40.0 51.1 34447.4
6 [35, 65] 30.0 40.6 24832.8
7 [40, 60] 20.0 26.5 16087.9
8 [45, 55] 10.0 12.5 8026.3

Predicción bootstraping, cuantiles y distribución

En las secciones anteriores se ha mostrado el uso del proceso de backtesting para estimar el intervalo de predicción a lo largo de un periodo de tiempo determinado. El objetivo es imitar el comportamiento del modelo en producción ejecutando predicciones a intervalos regulares, actualizando incrementalmente los datos de entrada.

También, es posible ejecutar una única predicción que estime N strps por delante sin pasar por todo el proceso de backtesting. En estos casos, skforecast ofrece cuatro métodos diferentes: predict_bootstrapping, predict_interval, predict_quantile y predict_distribution. Para información detallada sobre estos métodos, consulte la documentación.

Conformal Prediction

Conformal prediction engloba un conjunto de técnicas que permiten generar intervalos de predicción garantizando una determinada cobertura. Se basa en combinar las predicciones puntuales de un modelo de forecasting con sus residuales históricos (diferencias entre predicciones previas y valores observados). Estos residuales permiten estimar la incertidumbre en la predicción y ajustar la amplitud del intervalo entorno a la predicción puntual. Skforecast utiliza Split Conformal Prediction (SCP).


Conformal regression convierte las predicciones puntuales en intervalos de predicción. Fuente: Introduction To Conformal Prediction With Python: A Short Guide For Quantifying Uncertainty Of Machine Learning Models by Christoph Molnar https://leanpub.com/conformal-prediction

Este método se puede utilizar también para calibrar los intervalos de predicción generados por otros métodos, como la regresión cuantílica o bootstrapping. En estos casos, el método conformal ajusta los intervalos para garantizar que sigan siendo válidos con respecto a una determinada de cobertura.

Warning

Existen varios métodos de conformal prediction bien establecidos, cada uno con sus propias características y suposiciones. Sin embargo, cuando se aplican al forecasting de series temporales, sus garantías de cobertura solo son válidas para predicciones de un paso adelante (*single-step*). Para predicciones de varios pasos adelante (*multi-step*), la cobertura no está garantizada. Skforecast implementa *Split Conformal Prediction (SCP)* debido a su balance entre complejidad y eficacia.

Se realiza un proceso de backtesting para estimar los intervalos de predicción para el conjunto de test, esta vez utilizando el método conformal. Dado que los residuos outsample ya están almacenados en el objeto forecaster, el argumento use_in_sample_residuals se establece en False, y use_binned_residuals se establece en True para permitir intervalos adaptativos.

# Backtesting con intervalos en los datos de test usando residuos out-sample
# ==============================================================================
cv = TimeSeriesFold(steps = 24, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = 0.8,  # 80% prediction interval
                          interval_method         = 'conformal',
                          use_in_sample_residuals = False,  # Use out-sample residuals
                          use_binned_residuals    = True,   # Adaptive conformal
                      )
predictions.head(3)
  0%|          | 0/19 [00:00<?, ?it/s]
pred lower_bound upper_bound
2012-10-02 00:00:00 60.399647 32.785300 88.013993
2012-10-02 01:00:00 17.617464 8.692943 26.541985
2012-10-02 02:00:00 9.002365 0.077843 17.926886
# Gráfico intervalos
# ==============================================================================
plot_predicted_intervals(
    predictions     = predictions,
    y_true          = data_test,
    target_variable = "users",
    xaxis_title     = "Date time",
    yaxis_title     = "users",
)

# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = calculate_coverage(
               y_true      = data.loc[end_validation:, 'users'],
               lower_bound = predictions["lower_bound"], 
               upper_bound = predictions["upper_bound"]
           )
print(f"Predicted interval coverage: {round(100 * coverage, 2)} %")

# Area del intervalo
# ==============================================================================
area = (predictions["upper_bound"] - predictions["lower_bound"]).sum()
print(f"Area of the interval: {round(area, 2)}")
Oct 32012Oct 5Oct 7Oct 9Oct 11Oct 13Oct 15Oct 17Oct 1902004006008001000
Real valuePredictionDate timeusers
Predicted interval coverage: 78.29 %
Area of the interval: 64139.13

Los intervalos obtenidos muestra una cobertura ligeramente inferior a la 80% esperada, pero cercana a ella.

Regresión cuantílica

A diferencia de los modelos de regresión más comunes, que pretende estimar la media de la variable respuesta dados ciertos valores de las variables predictoras, la regresión cuantílica tiene como objetivo estimar los cuantiles condicionales de la variable respuesta. Para una función de distribución continua, el cuantil α Qα(x) se define como el valor tal que la probabilidad de que Y sea menor que Qα(x) es, para un determinado X=x, igual a α. Por ejemplo, el 36% de los valores de la población son inferiores al cuantil Q=0,36. El cuantil más conocido es el cuantil 50%, más comúnmente conocido como mediana.

Al combinar las predicciones de dos modelos de regresión cuantílica, es posible construir un intervalo donde, cada modelo, estima uno de los límites del intervalo. Por ejemplo, los modelos obtenidos para Q=0.1 y Q=0.9 generan un intervalo de predicción del 80% (90% - 10% = 80%).

Son varios los algoritmos de machine learning capaces de modelar cuantiles. Algunos de ellos son:

Así como el error cuadrático se utiliza como función de coste para entrenar modelos que predicen el valor medio, se necesita una función de coste específica para entrenar modelos que predicen cuantiles. La función utilizada con más frecuencia para la regresión de cuantiles se conoce como pinball:

pinball(y,ˆy)=1nmuestrasnmuestras1i=0αmax(yiˆyi,0)+(1α)max(ˆyiyi,0)

donde α es el cuantil objetivo, y el valor real e ˆy la predicción del cuantil.

Se puede observar que el coste difiere según el cuantil evaluado. Cuanto mayor sea el cuantil, más se penalizan las subestimaciones y menos las sobreestimaciones. Al igual que con MSE y MAE, el objetivo es minimizar sus valores (a menor coste, mejor).

Dos desventajas de la regresión por cuantiles en comparación con el método de bootstrapping son: que cada cuantil necesita su regresor, y que la regresión por cuantiles no está disponible para todos los tipos de modelos de regresión. Sin embargo, una vez entrenados los modelos, la inferencia es mucho más rápida ya que no se necesita un proceso iterativo.

Este tipo de intervalos de predicción se pueden estimar fácilmente utilizando ForecasterDirect.

Warning

Los forecasters de tipo ForecasterDirect son más lentos que ForecasterRecursive porque requieren entrenar un modelo por paso. Aunque pueden lograr un mejor rendimiento, su escalabilidad es una limitación importante cuando se necesitan predecir muchos pasos.

# Crear forecasters: uno para cada límite del intervalo
# ==============================================================================
# Los forecasters obtenidos para alpha=0.1 y alpha=0.9 producen un intervalo de
# confianza del 80% (90% - 10% = 80%).

# Forecaster para cuantil 10%
forecaster_q10 = ForecasterDirect(
                     regressor = LGBMRegressor(
                                     objective    = 'quantile',
                                     metric       = 'quantile',
                                     alpha        = 0.1,
                                     random_state = 15926,
                                     verbose      = -1
                                     
                                 ),
                     lags  = lags,
                     steps = 24
                 )

# Forecaster para cuantil 90%
forecaster_q90 = ForecasterDirect(
                     regressor = LGBMRegressor(
                                     objective    = 'quantile',
                                     metric       = 'quantile',
                                     alpha        = 0.9,
                                     random_state = 15926,
                                     verbose      = -1
                                     
                                 ),
                     lags  = lags,
                     steps = 24
                 )

Una vez definidos los forecasters, se realiza una búsqueda bayesiana para encontrar los mejores hiperparámetros para los regresores. Al validar un modelo de regresión cuantílica, es importante utilizar una métrica coherente con el cuantil que se está evaluando. En este caso, se utiliza función de coste pinball. Skforecast proporciona la función create_mean_pinball_loss para calcular la función de coste pinball para un cuantil dado.

# Grid search para los hyper-parámetros y lags de cada quantile-forecaster
# ==============================================================================
def search_space(trial):
    search_space  = {
        'n_estimators'  : trial.suggest_int('n_estimators', 100, 500, step=50),
        'max_depth'     : trial.suggest_int('max_depth', 3, 10, step=1),
        'learning_rate' : trial.suggest_float('learning_rate', 0.01, 0.1)
    }

    return search_space

cv = TimeSeriesFold(steps = 24, initial_train_size = len(data[:end_train]))

results_grid_q10 = bayesian_search_forecaster(
                       forecaster   = forecaster_q10,
                       y            = data.loc[:end_validation, 'users'],
                       cv           = cv,
                       metric       = create_mean_pinball_loss(alpha=0.1),
                       search_space = search_space,
                       n_trials     = 10
                   )

results_grid_q90 = bayesian_search_forecaster(
                       forecaster   = forecaster_q90,
                       y            = data.loc[:end_validation, 'users'],
                       cv           = cv,
                       metric       = create_mean_pinball_loss(alpha=0.9),
                       search_space = search_space,
                       n_trials     = 10
                   )
  0%|          | 0/10 [00:00<?, ?it/s]
`Forecaster` refitted using the best-found lags and parameters, and the whole data set: 
  Lags: [  1   2   3  23  24  25 167 168 169] 
  Parameters: {'n_estimators': 450, 'max_depth': 8, 'learning_rate': 0.06499211596098246}
  Backtesting metric: 21.106815983551137
  0%|          | 0/10 [00:00<?, ?it/s]
`Forecaster` refitted using the best-found lags and parameters, and the whole data set: 
  Lags: [  1   2   3  23  24  25 167 168 169] 
  Parameters: {'n_estimators': 400, 'max_depth': 5, 'learning_rate': 0.042560979006008276}
  Backtesting metric: 38.44122580678526

Una vez que se han encontrado los mejores hiperparámetros para cada forecaster, se aplica un proceso de backtesting utilizando los datos de test.

# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(steps = 24, initial_train_size = len(data.loc[:end_validation]))
metric_q10, predictions_q10 = backtesting_forecaster(
                                  forecaster = forecaster_q10,
                                  y          = data['users'],
                                  cv         = cv,
                                  metric     = create_mean_pinball_loss(alpha=0.1)
                              )

metric_q90, predictions_q90 = backtesting_forecaster(
                                  forecaster = forecaster_q90,
                                  y          = data['users'],
                                  cv         = cv,
                                  metric     = create_mean_pinball_loss(alpha=0.9)
                              )
predictions = pd.concat([predictions_q10, predictions_q90], axis=1)
predictions.columns = ['lower_bound', 'upper_bound']
predictions.head(3)
  0%|          | 0/19 [00:00<?, ?it/s]
  0%|          | 0/19 [00:00<?, ?it/s]
lower_bound upper_bound
2012-10-02 00:00:00 39.108177 73.136230
2012-10-02 01:00:00 11.837010 32.588538
2012-10-02 02:00:00 4.453201 14.346566
# Gráfico
# ==============================================================================
fig = go.Figure([
    go.Scatter(name='Real value', x=data_test.index, y=data_test['users'], mode='lines'),
    go.Scatter(
        name='Upper Bound', x=predictions.index, y=predictions['upper_bound'],
        mode='lines', marker=dict(color="#444"), line=dict(width=0), showlegend=False
    ),
    go.Scatter(
        name='Lower Bound', x=predictions.index, y=predictions['lower_bound'],
        marker=dict(color="#444"), line=dict(width=0), mode='lines',
        fillcolor='rgba(68, 68, 68, 0.3)', fill='tonexty', showlegend=False
    )
])
fig.update_layout(
    title="Real value vs predicted in test data",
    xaxis_title="Date time",
    yaxis_title="users",
    width=800,
    height=400,
    margin=dict(l=20, r=20, t=35, b=20),
    hovermode="x",
    legend=dict(orientation="h", yanchor="top", y=1.1, xanchor="left", x=0.001)
)
fig.show()
Oct 32012Oct 5Oct 7Oct 9Oct 11Oct 13Oct 15Oct 17Oct 1902004006008001000
Real valueReal value vs predicted in test dataDate timeusers
# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = calculate_coverage(
               y_true      = data.loc[end_validation:, 'users'],
               lower_bound = predictions["lower_bound"], 
               upper_bound = predictions["upper_bound"]
           )
print(f"Cobertura del intervalo: {round(100 * coverage, 2)} %")

# Area del intervalo
# ==============================================================================
area = (predictions_q90["pred"] - predictions_q10["pred"]).sum()
print(f"Area del intervalo: {round(area, 2)}")
Cobertura del intervalo: 62.28 %
Area del intervalo: 109062.92

En este caso de uso, la estrategia de previsión cuantílica no logra una cobertura empírica cercana a la esperada (80%).

Calibración externa de intervalos de predicción

Con frecuencia, los intervalos de predicción obtenidos con los diferentes métodos no logran la cobertura deseada porque son demasiado optimistas o pesimistas. Para abordar este problema, skforecast proporciona el transformador ConformalIntervalCalibrator, que se puede utilizar para calibrar los intervalos de predicción obtenidos con otros métodos.

Información de sesión

import session_info
session_info.show(html=False)
-----
feature_engine      1.8.3
lightgbm            4.6.0
matplotlib          3.9.4
numpy               2.1.3
pandas              2.2.3
plotly              6.0.0
session_info        1.0.0
skforecast          0.15.0
sklearn             1.6.1
-----
IPython             8.32.0
jupyter_client      8.6.3
jupyter_core        5.7.2
-----
Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb  6 2025, 18:49:16) [MSC v.1929 64 bit (AMD64)]
Windows-11-10.0.26100-SP0
-----
Session information updated at 2025-03-13 11:15

Instrucciones para citar

¿Cómo citar este documento?

Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!

Forecasting probabilistico con machine learning por Joaquín Amat Rodrigo y Javier Escobar Ortiz, , disponible bajo una licencia Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) en https://www.cienciadedatos.net/documentos/py42-foreasting-probabilistico.html

¿Cómo citar skforecast?

Si utilizas skforecast, te agradecemos mucho que lo cites. ¡Muchas gracias!

Zenodo:

Amat Rodrigo, Joaquin, & Escobar Ortiz, Javier. (2024). skforecast (v0.15.0). Zenodo. https://doi.org/10.5281/zenodo.8382788

APA:

Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast (Version 0.15.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.15.0}, month = {03}, year = {2025}, 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! 😊

Become a GitHub Sponsor Become a GitHub Sponsor

Creative Commons Licence

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.