Forecasting probabilistico con Machine Learning

Si te gusta  Skforecast , ayúdanos dándonos una estrella en  GitHub! ⭐️

Forecasting probabilístico con machine learning

Joaquín Amat Rodrigo, Javier Escobar Ortiz
Abril, 2022 (última actualización Noviembre 2024)

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.

Existen múltiples formas de estimar intervalos de predicción en modelos de forecasting, la mayoría de las cuales requieren que los residuos (errores) del modelo sigan una distribución normal. Cuando no se puede asumir esta propiedad, dos alternativas comúnmente utilizadas son el bootstrapping y la regresión cuantílica. Para ilustrar cómo la librería skforecast permite estimar intervalos de predicción en modelos de forecasting multi-step se muestran dos ejemplo:

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
  • 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.

✎ Note

Conformal prediction es un método relativamente nuevo que permite estimar la incertidumbre aociada a las predicciones de modelos de machine learning. Este método está en la hoja de ruta de skforecast, pero aún no está disponible.

Intervalos de predicción utilizando bootstrapping de los residuos

El error en la predicción del siguiente valor de una serie (one-step-ahead forecast) se define como $e_t = y_t - \hat{y}_{t|t-1}$. 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 $\alpha/2$ y $1−\alpha/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

In [1]:
# Preprocesado de datos
# ==============================================================================
import numpy as np
import pandas as pd
from skforecast.datasets import fetch_dataset

# Plots
# ==============================================================================
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
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 skforecast.plot import plot_prediction_distribution
from pprint import pprint

# Modelado y Forecasting
# ==============================================================================
import skforecast
import sklearn
import lightgbm
from lightgbm import LGBMRegressor
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from skforecast.recursive import ForecasterRecursive
from skforecast.direct import ForecasterDirect
from skforecast.model_selection import TimeSeriesFold
from skforecast.model_selection import bayesian_search_forecaster
from skforecast.model_selection import backtesting_forecaster
from sklearn.metrics import mean_pinball_loss
from scipy.stats import norm

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

color = '\033[1m\033[38;5;208m'
print(f"{color}Versión skforecast: {skforecast.__version__}")
print(f"{color}Versión scikit-learn: {sklearn.__version__}")
print(f"{color}Version lightgbm: {lightgbm.__version__}")
print(f"{color}Versión pandas: {pd.__version__}")
print(f"{color}Versión numpy: {np.__version__}")
Versión skforecast: 0.14.0
Versión scikit-learn: 1.5.2
Version lightgbm: 4.4.0
Versión pandas: 2.2.3
Versión numpy: 2.0.2

Datos

In [2]:
# Descarga de datos
# ==============================================================================
data = fetch_dataset(name='bike_sharing_extended_features')
data.head(2)
bike_sharing_extended_features
------------------------------
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, the dataset
was enriched by introducing supplementary features. Addition includes calendar-
based variables (day of the week, hour of the day, month, etc.), indicators for
sunlight, incorporation of rolling temperature averages, and the creation of
polynomial features generated from variable pairs. All cyclic variables are
encoded using sine and cosine functions to ensure accurate representation.
Fanaee-T,Hadi. (2013). Bike Sharing Dataset. UCI Machine Learning Repository.
https://doi.org/10.24432/C5W894.
Shape of the dataset: (17352, 90)
Out[2]:
users weather month_sin month_cos week_of_year_sin week_of_year_cos week_day_sin week_day_cos hour_day_sin hour_day_cos ... temp_roll_mean_1_day temp_roll_mean_7_day temp_roll_max_1_day temp_roll_min_1_day temp_roll_max_7_day temp_roll_min_7_day holiday_previous_day holiday_next_day temp holiday
date_time
2011-01-08 00:00:00 25.0 mist 0.5 0.866025 0.120537 0.992709 -0.781832 0.62349 0.258819 0.965926 ... 8.063334 10.127976 9.02 6.56 18.86 4.92 0.0 0.0 7.38 0.0
2011-01-08 01:00:00 16.0 mist 0.5 0.866025 0.120537 0.992709 -0.781832 0.62349 0.500000 0.866025 ... 8.029166 10.113334 9.02 6.56 18.86 4.92 0.0 0.0 7.38 0.0

2 rows × 90 columns

In [3]:
# One hot encoding de las variables categóricas
# ==============================================================================
encoder = ColumnTransformer(
              [('one_hot_encoder', OneHotEncoder(sparse_output=False), ['weather'])],
              remainder='passthrough',
              verbose_feature_names_out=False
          ).set_output(transform="pandas")
data = encoder.fit_transform(data)
In [4]:
# Selección de las variables exógenas
# ==============================================================================
exog_features = [
    'weather_clear', 'weather_mist', 'weather_rain', 'month_sin', 'month_cos',
    'week_of_year_sin', 'week_of_year_cos', 'week_day_sin', 'week_day_cos',
    'hour_day_sin', 'hour_day_cos', 'sunrise_hour_sin', 'sunrise_hour_cos',
    'sunset_hour_sin', 'sunset_hour_cos', 'temp'
]
data = data[['users'] + exog_features]

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.

In [5]:
# Partición de datos en entrenamiento-validación-test
# ==============================================================================
data = data.loc['2011-05-30 23:59:00':, :]
end_train = '2012-08-30 23:59:00'
end_validation = '2012-11-15 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-05-31 00:00:00 --- 2012-08-30 23:00:00  (n=10992)
Dates validacion : 2012-08-31 00:00:00 --- 2012-11-15 23:00:00  (n=1848)
Dates test       : 2012-11-16 00:00:00 --- 2012-12-30 23:00:00  (n=1080)

Exploración gráfica

La exploración gráfica de series temporales puede ser una forma eficaz de identificar tendencias, patrones y variaciones estacionales. Esto, a su vez, ayuda a orientar la selección del modelo de forecasting más adecuado.

In [6]:
# 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=750,
    height=350,
    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()

Los gráficos de autocorrelación muestran la correlación entre una serie temporal y sus valores pasados. Son una herramienta útil para identificar el orden de un modelo autorregresivo, es decir, los valores pasados (lags) que se deben incluir en el modelo.

La función de autocorrelación (ACF) mide la correlación entre una serie temporal y sus valores pasados. La función de autocorrelación parcial (PACF) mide la correlación entre una serie temporal y sus valores pasados, pero solo después de eliminar las variaciones explicadas por los valores pasados intermedios.

In [7]:
# Gráfico de autocorrelación
# ==============================================================================
fig, ax = plt.subplots(figsize=(6, 2))
plot_acf(data.users, ax=ax, lags=24 * 3)
plt.show()
In [8]:
# Gráfico autocorrelación parcial
# ==============================================================================
fig, ax = plt.subplots(figsize=(6, 2))
plot_pacf(data.users, ax=ax, lags=24 * 3)
plt.show()

Los resultados del estudio de autocorrelación indican una correlación significativa entre el número de usuarios en las horas anteriores, así como en los días previos. Esto significa que conocer del número de usuarios durante periodos específicos del pasado proporciona información útil para predecir el número de usuarios en el futuro.

Residuos in-sample

Se entrena un recursive-multi-step forecaster y se optimiza sus hiperparámetros. Luego, se estiman los intervalos de predicción utilizando bootstrapping.

In [9]:
# Create forecaster and hyperparameters search
# ==============================================================================
# Forecaster
forecaster = ForecasterRecursive(
                regressor = LGBMRegressor(random_state=15926, verbose=-1),
                lags = 7 
            )

# Lags used as predictors
lags_grid = [24, 48, (1, 2, 3, 23, 24, 25, 47, 48, 49, 71, 72, 73, 364*24, 365*24)]

# Particiones
cv = TimeSeriesFold(
        steps              = 24,
        initial_train_size = len(data[:end_train]),
        refit              = False,
     )

# Regressor hyperparameters search space
def search_space(trial):
    search_space  = {
        'lags'            : trial.suggest_categorical('lags', lags_grid),
        'n_estimators'    : trial.suggest_int('n_estimators', 200, 800, step=100),
        'max_depth'       : trial.suggest_int('max_depth', 3, 8, step=1),
        'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 25, 500),
        'learning_rate'   : trial.suggest_float('learning_rate', 0.01, 0.5),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.5, 0.8, step=0.1),
        'max_bin'         : trial.suggest_int('max_bin', 50, 100, step=25),
        'reg_alpha'       : trial.suggest_float('reg_alpha', 0, 1, step=0.1),
        'reg_lambda'      : trial.suggest_float('reg_lambda', 0, 1, step=0.1)
    }
    return search_space

results_search, frozen_trial = bayesian_search_forecaster(
                                   forecaster    = forecaster,
                                   y             = data.loc[:end_validation, 'users'],
                                   exog          = data.loc[:end_validation, exog_features],
                                   cv            = cv,
                                   metric        = 'mean_absolute_error',
                                   search_space  = search_space,
                                   n_trials      = 20,
                                   random_state  = 123,
                                   return_best   = True,
                                   n_jobs        = 'auto',
                                   verbose       = False,
                                   show_progress = True
                               )
best_params = results_search['params'].iloc[0]
best_lags   = results_search['lags'].iloc[0]
`Forecaster` refitted using the best-found lags and parameters, and the whole data set: 
  Lags: [   1    2    3   23   24   25   47   48   49   71   72   73 8736 8760] 
  Parameters: {'n_estimators': 300, 'max_depth': 5, 'min_data_in_leaf': 25, 'learning_rate': 0.10794173876361574, 'feature_fraction': 0.6, 'max_bin': 100, 'reg_alpha': 1.0, 'reg_lambda': 1.0}
  Backtesting metric: 55.26098214742841

Una vez encontrados los mejores hiperparámetros, se utiliza la función backtesting_forecaster() para generar los intervalos de predicción de todo el conjunto de test.

  • El argumento interval se emplea para especificar la probabilidad de cobertura deseada de los intervalos de predicción. En este caso, interval se establece en [10, 90], lo que 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%.

  • El argumento n_boot se utiliza para especificar el número de muestras bootstrap que se utilizarán para estimar los intervalos de predicción. Cuanto mayor sea el número de muestras, más precisos serán los intervalos de predicción, pero mayor el tiempo necesario.

Por defecto, los intervalos se calculan utilizando los residuos in-sample (residuos del conjunto de entrenamiento). Esto puede dar lugar a intervalos demasiado estrechos (demasiado optimistas).

In [10]:
# Backtesting con intervalos en los datos de test usando residuos in-sample
# ==============================================================================
cv = TimeSeriesFold(
        steps              = 24,
        initial_train_size = len(data.loc[:end_validation]),
        refit              = False,
    )
metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = [10, 90],
                          n_boot                  = 250,
                          use_in_sample_residuals = True,
                          use_binned_residuals    = False,
                          n_jobs                  = 'auto',
                          verbose                 = False,
                          show_progress           = True
                      )
display(metric)
predictions.head(5)
mean_absolute_error
0 50.462723
Out[10]:
pred lower_bound upper_bound
2012-11-16 00:00:00 75.957987 52.697966 100.885160
2012-11-16 01:00:00 22.895924 0.953096 56.633349
2012-11-16 02:00:00 11.105227 -12.096016 41.032015
2012-11-16 03:00:00 7.465197 -19.899672 34.476395
2012-11-16 04:00:00 6.216360 -15.973349 39.723453
In [11]:
# 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=750,
        height=350,
        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()


def empirical_coverage(y, lower_bound, upper_bound):
    """
    Calculate coverage of a given interval
    """
    return np.mean(np.logical_and(y >= lower_bound, y <= upper_bound))
In [12]:
# Gráfico intervalos
# ==============================================================================
plot_predicted_intervals(
    predictions     = predictions,
    y_true          = data_test,
    target_variable = "users",
    initial_x_zoom  = ['2012-12-01', '2012-12-25'],
    title           = "Valor real vs predicciones en el conjunto de test",
    xaxis_title     = "Date time",
    yaxis_title     = "users",
) 

# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = empirical_coverage(
                y           = 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)}")
Cobertura del intervalo: 58.15 %
Area del intervalo: 82767.54

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.

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

El método set_out_sample_residuals() se utiliza para almacenar residuos fuera de muestra (out-sample) calculados con un conjunto de validación a través de backtesting. Una vez añadidos los nuevos residuos al foreacster, se tiene que indicar use_in_sample_residuals a False para que el forecaster los utilice.

In [13]:
# Backtesting con datos de validación para obtener residuos out-sample
# ==============================================================================
cv = TimeSeriesFold(
        steps              = 24,
        initial_train_size = len(data.loc[:end_train]),
        refit              = False,
    )
_, 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',
                         n_jobs        = 'auto',
                         verbose       = False,
                         show_progress = True
                     )
In [14]:
# Distribución residuos out-sample
# ==============================================================================
residuals = data.loc[predictions_val.index, 'users'] - predictions_val['pred']
print(pd.Series(np.where(residuals < 0, 'negarivo', 'positivo')).value_counts())
plt.rcParams.update({'font.size': 8})
_ = plot_residuals(residuals=residuals, figsize=(7, 4))
positivo    1029
negarivo     819
Name: count, dtype: int64
In [15]:
# 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']
)
In [16]:
# Backtesting con intervalos en los datos de test usando residuos out-sample
# ==============================================================================
cv = TimeSeriesFold(
        steps              = 24,
        initial_train_size = len(data.loc[:end_validation]),
        refit              = False,
    )
metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = [10, 90],
                          n_boot                  = 250,
                          use_in_sample_residuals = False, # Usar residuos out-sample
                          use_binned_residuals    = False,
                          n_jobs                  = 'auto',
                          verbose                 = False,
                          show_progress           = True
                      )
predictions.head(5)
Out[16]:
pred lower_bound upper_bound
2012-11-16 00:00:00 75.957987 3.867451 179.864239
2012-11-16 01:00:00 22.895924 -17.751696 175.045007
2012-11-16 02:00:00 11.105227 -40.826370 168.447203
2012-11-16 03:00:00 7.465197 -28.237689 193.401750
2012-11-16 04:00:00 6.216360 -40.160433 184.578289
In [17]:
# Gráfico intervalos
# ==============================================================================
plot_predicted_intervals(
    predictions     = predictions,
    y_true          = data_test,
    target_variable = "users",
    initial_x_zoom  = ['2012-12-01', '2012-12-25'],
    title           = "Valor real vs predicciones en el conjunto de test",
    xaxis_title     = "Date time",
    yaxis_title     = "users",
)

# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = empirical_coverage(
                y           = 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)}")
Cobertura del intervalo: 83.43 %
Area del intervalo: 255879.67

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.

Residuos Out-sample (condicionados a los valores predichos)

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 un sklearn.preprocessing.KBinsDiscretizer con los parámetros: n_bins=15, encode=ordinal, strategy=quantile, subsample=10000, random_state=789654, dtype=np.float64. El proceso de binning puede ajustarse utilizando el argumento binner_kwargs del objeto forecaster.

In [18]:
# Crear forecaster
# ==============================================================================
forecaster = ForecasterRecursive(
                regressor     = LGBMRegressor(random_state=15926, verbose=-1, **best_params),
                lags          = best_lags,
                binner_kwargs = {'n_bins': 15}
            )

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

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 prediccho con el que están relacionados. Aunque no se utilizan en este ejemplo, los residuos in-sample también se dividen en intervalos y se almacenan en el atributo in_sample_residuals_by_bin_.

In [19]:
# Intervalos de los bins
# ==============================================================================
pprint(forecaster.binner_intervals_)
{0.0: (-1.741708189819649, 10.317040266602518),
 1.0: (10.317040266602518, 23.397712684935296),
 2.0: (23.397712684935296, 42.81143589390928),
 3.0: (42.81143589390928, 93.70601153260277),
 4.0: (93.70601153260277, 146.00472540188062),
 5.0: (146.00472540188062, 185.98114181351224),
 6.0: (185.98114181351224, 227.94516357708295),
 7.0: (227.94516357708295, 263.17131420814536),
 8.0: (263.17131420814536, 298.7107366143719),
 9.0: (298.7107366143719, 339.1431337029709),
 10.0: (339.1431337029709, 399.04627830804475),
 11.0: (399.04627830804475, 472.88850088361374),
 12.0: (472.88850088361374, 553.7082345948482),
 13.0: (553.7082345948482, 682.5049823044178),
 14.0: (682.5049823044178, 994.5807380527069)}

A continuación, los residuos out-sample se almacenan en el forecaster. Para evitar un uso excesivo de memoria, se almacena un máximo de 10000//n_bins residuos por partición (bin).

In [20]:
# 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']
)
In [21]:
# 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=148
 Bin 1: n=118
 Bin 2: n=107
 Bin 3: n=117
 Bin 4: n=121
 Bin 5: n=133
 Bin 6: n=116
 Bin 7: n=106
 Bin 8: n=161
 Bin 9: n=138
 Bin 10: n=126
 Bin 11: n=125
 Bin 12: n=134
 Bin 13: n=118
 Bin 14: n=80
In [22]:
# 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");

Los gráficos de caja ilustran cómo tanto la dispersión como la magnitud de los residuos varían con los valores predichos. Por ejemplo, en el bin 0, los residuos se mantienen dentro de un valor absoluto de 100, mientras que en los bins mayores a 5 superan con frecuencia este umbral.

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.

In [23]:
# Backtesting con intervalos en los datos de test usando residuos out-sample
# ==============================================================================
cv = TimeSeriesFold(
        steps              = 24,
        initial_train_size = len(data.loc[:end_validation]),
        refit              = False,
    )

metric, predictions = backtesting_forecaster(
                          forecaster              = forecaster,
                          y                       = data['users'],
                          exog                    = data[exog_features],
                          cv                      = cv,
                          metric                  = 'mean_absolute_error',
                          interval                = [10, 90],
                          n_boot                  = 250,
                          use_in_sample_residuals = False, # Usar residuos out-sample
                          use_binned_residuals    = True,  # Usar residuos por bin
                          n_jobs                  = 'auto',
                          verbose                 = False,
                          show_progress           = True
                      )
predictions.head(5)
Out[23]:
pred lower_bound upper_bound
2012-11-16 00:00:00 75.957987 42.852151 153.661841
2012-11-16 01:00:00 22.895924 13.017612 90.814613
2012-11-16 02:00:00 11.105227 5.301970 43.867037
2012-11-16 03:00:00 7.465197 4.394094 23.024220
2012-11-16 04:00:00 6.216360 3.496328 19.235150
In [24]:
# Gráfico intervalos
# ==============================================================================
plot_predicted_intervals(
    predictions     = predictions,
    y_true          = data_test,
    target_variable = "users",
    initial_x_zoom  = ['2012-12-01', '2012-12-25'],
    title           = "Valor real vs predicciones en el conjunto de test",
    xaxis_title     = "Date time",
    yaxis_title     = "users",
)

# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = empirical_coverage(
                y           = 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)}")
Cobertura del intervalo: 78.89 %
Area del intervalo: 209530.0

Cuando se utilizan residuos out-sample condicionados al valor predicho, el intervalo tiene una cobertura próxima al valor esperado (80%) a la vez que se reduce su amplitud. El modelo es capaz de distribuir mejor la incertidumbre en sus predicciones.

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.

Predict Bootstraping

El método predict_bootstrapping realiza las iteraciones de bootstrapping para generar la muestra de posibles predicciones alternativas. Estos son los valores subyacentes utilizados para calcular los intervalos, cuantiles y distribuciones.

In [25]:
# Entrenar forecaster
# ==============================================================================
forecaster.fit(
    y     = data.loc[:end_validation, 'users'],
    exog  = data.loc[:end_validation, exog_features]
)
In [26]:
# Predecir 10 secuencias diferentes de 7 steps cada una usando bootstrapping
# ==============================================================================
boot_predictions = forecaster.predict_bootstrapping(
                       exog   = data_test[exog_features],
                       steps  = 7,
                       n_boot = 25
                   )
boot_predictions
Out[26]:
pred_boot_0 pred_boot_1 pred_boot_2 pred_boot_3 pred_boot_4 pred_boot_5 pred_boot_6 pred_boot_7 pred_boot_8 pred_boot_9 ... pred_boot_15 pred_boot_16 pred_boot_17 pred_boot_18 pred_boot_19 pred_boot_20 pred_boot_21 pred_boot_22 pred_boot_23 pred_boot_24
2012-11-16 00:00:00 75.072526 67.917014 90.497835 72.887786 69.451738 65.636227 67.752751 74.731177 87.611129 82.144507 ... 90.132866 136.833471 70.875736 66.419248 66.151284 74.751401 48.791706 68.656787 83.055140 68.128650
2012-11-16 01:00:00 33.217798 39.088561 53.430776 8.530052 31.098442 19.103678 25.434839 39.863960 53.959646 -0.065547 ... 77.283496 103.139985 16.806962 24.998564 16.564876 8.105737 25.639537 47.999683 21.557125 21.669114
2012-11-16 02:00:00 30.117753 8.819270 21.098243 6.527203 -2.097282 11.071403 8.829349 40.070996 9.417611 -24.525254 ... 40.248554 47.861372 41.019650 -5.893293 3.011746 -13.445515 45.709909 -11.076396 24.972022 20.706668
2012-11-16 03:00:00 -2.370557 2.628568 8.867311 -9.985030 -1.431195 24.763869 2.948100 -0.293142 8.415971 100.438948 ... 1.438511 4.828710 21.165506 7.745074 -24.641198 4.783921 60.887626 17.057142 -9.878423 -22.821821
2012-11-16 04:00:00 33.421322 -5.012262 18.803097 25.102996 32.054605 14.578793 52.905294 -41.144396 16.813604 107.710606 ... -28.544466 1.939137 -11.529134 -11.649420 43.438586 -0.654120 23.865921 7.612053 5.411233 -1.574533
2012-11-16 05:00:00 81.259318 58.335735 23.256711 51.950788 51.408044 53.162599 62.032153 52.793819 42.212314 106.453046 ... 44.910476 -2.765791 15.125106 -17.912916 96.705426 30.322670 36.136752 34.202833 38.794849 35.524510
2012-11-16 06:00:00 195.059101 195.716369 125.295581 197.557367 171.269419 170.727804 175.102145 160.966413 157.567075 155.195939 ... 152.996113 123.790988 98.159621 100.287313 192.879471 125.427235 89.445302 129.648327 178.383805 136.410872

7 rows × 25 columns

Un gráfico de ridge es una forma útil de visualizar la incertidumbre asociada a las predicciones de un modelo de forecasting. Este gráfico muestra la distribución de desnidad para cada step utilizando las predicciones obtenidas por bootstrapping.

In [27]:
# Ridge plot de las distribuciones de bootstrapping
# ==============================================================================
_ = plot_prediction_distribution(boot_predictions, figsize=(7, 4))

Predict Interval

En la mayoría de los casos, el usuario está interesado en un intervalo específico en lugar de toda la matriz de predicciones obtenida por bootstrapping. Para cubrir esta necesidad, skforecast proporciona el método predict_interval. Este método utiliza internamente predict_bootstrapping y estima los cuantiles superior e inferior para cada step, proporcionando así los intervalos de predicción deseados.

In [28]:
# Predicción de intervalos para los siguiente 7 steps
# ==============================================================================
predictions = forecaster.predict_interval(
                  exog     = data_test[exog_features],
                  steps    = 7,
                  interval = [10, 90],
                  n_boot   = 150
              )
predictions
Out[28]:
pred lower_bound upper_bound
2012-11-16 00:00:00 75.957987 55.871660 106.509594
2012-11-16 01:00:00 22.895924 2.862862 56.508504
2012-11-16 02:00:00 11.105227 -13.861631 41.666618
2012-11-16 03:00:00 7.465197 -14.729066 40.250606
2012-11-16 04:00:00 6.216360 -16.066013 43.949188
2012-11-16 05:00:00 31.850292 10.522605 66.098993
2012-11-16 06:00:00 145.218401 96.256977 178.714044

Predict Quantile

Este método funciona de forma similar a predict_interval, con la característica añadida de que permite definir una lista específica de cuantiles. Es importante recordar que estos cuantiles deben estar dentro del intervalo [0, 1].

In [29]:
# Predicción de cuantiles 5, 25, 75 and 95 para los siguientes 7 steps 
# ==============================================================================
predictions = forecaster.predict_quantiles(
                  exog      = data_test[exog_features],
                  steps     = 7,
                  n_boot    = 150,
                  quantiles = [0.05, 0.25, 0.75, 0.95],
              )
predictions
Out[29]:
q_0.05 q_0.25 q_0.75 q_0.95
2012-11-16 00:00:00 48.288075 67.642808 90.037329 118.281854
2012-11-16 01:00:00 -3.616814 16.409867 37.915283 64.523189
2012-11-16 02:00:00 -20.478988 -2.408681 22.836308 58.893143
2012-11-16 03:00:00 -28.116105 -2.513015 23.698522 51.932897
2012-11-16 04:00:00 -27.432799 -4.066136 22.940893 51.227532
2012-11-16 05:00:00 -4.006249 21.569188 49.501503 76.194038
2012-11-16 06:00:00 83.860754 114.212367 156.516781 189.210857

Predict Distribution

Las estimaciones de intervalos mostradas en los apartados anteriores no hacen ninguna asunción sobre una distribución concreta. El método predict_dist de skforecast permite ajustar una distribución paramétrica a las simulaciones obtenidas con predict_bootstrapping. Esto es útil cuando hay razones para creer que los errores de predicción siguen una distribución particular, como la distribución normal o la distribución t-student. El método predict_dist permite al usuario especificar cualquier distribución continua del módulo scipy.stats.

In [30]:
# Predicción de  los parámetros de una distribución normal en cada uno de los siguentes 7 steps
# ==============================================================================
predictions = forecaster.predict_dist(
                  exog         = data_test[exog_features],
                  steps        = 7,
                  n_boot       = 150,
                  distribution = norm
              )
predictions
Out[30]:
loc scale
2012-11-16 00:00:00 79.436270 21.590643
2012-11-16 01:00:00 27.275039 26.421942
2012-11-16 02:00:00 12.963605 25.253451
2012-11-16 03:00:00 9.804574 25.410589
2012-11-16 04:00:00 10.522987 25.929452
2012-11-16 05:00:00 34.254031 25.946783
2012-11-16 06:00:00 135.548288 36.410460

Intervalos de predicción utilizando modelos de 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 $\alpha$ $Q_{\alpha}(x)$ se define como el valor tal que la probabilidad de que $Y$ sea menor que $Q_{\alpha}(x)$ es, para un determinado $X=x$, igual a $\alpha$. 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:

$$\text{pinball}(y, \hat{y}) = \frac{1}{n_{\text{muestras}}} \sum_{i=0}^{n_{\text{muestras}}- 1} \alpha \max(y_i - \hat{y}_i, 0) + (1 - \alpha) \max(\hat{y}_i - y_i, 0)$$

donde $\alpha$ es el cuantil objetivo, $y$ el valor real e $\hat{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 forecaters de tipo ForecasterDirect son más lentos que los ForecasterRecursiveRecursive porque requieren el entrenamiento de un modelo por *step*. Para limitar el tiempo necesario para ejecutar los siguientes ejemplos, los datos se agregan a frecuencia diaria y sólo se predicen 7 los siguientes 7 dias (una semana).

Datos

In [31]:
# Descarga de datos
# ==============================================================================
data = fetch_dataset(name='bike_sharing_extended_features', verbose=False)
In [32]:
# Agragar datos a frecuencia horaria
# ==============================================================================
data = (
    data
    .resample(rule="D", closed="left", label="right")
    .agg({"users": "sum"})
)
In [33]:
# Particion train-validation-test
# ==============================================================================
end_train = '2012-05-31 23:59:00'
end_validation = '2012-09-15 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"Fechas train      : {data_train.index.min()} --- {data_train.index.max()}  (n={len(data_train)})")
print(f"Fechas validacion : {data_val.index.min()} --- {data_val.index.max()}  (n={len(data_val)})")
print(f"Fechas test       : {data_test.index.min()} --- {data_test.index.max()}  (n={len(data_test)})")
Fechas train      : 2011-01-09 00:00:00 --- 2012-05-31 00:00:00  (n=509)
Fechas validacion : 2012-06-01 00:00:00 --- 2012-09-15 00:00:00  (n=107)
Fechas test       : 2012-09-16 00:00:00 --- 2012-12-31 00:00:00  (n=107)

Modelos de regresión por cuantiles

Se estima un intervalo de predicción del 80% para los siguientes 7 días. En este ejemplo, se entrena un modelo de gradient boosting LightGBM, sin embargo, el lector puede utilizar cualquier otro modelo simplemente reemplazando la definición del regresor.

In [34]:
# 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  = 7,
                     steps = 7
                 )

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

Al validar un modelo de regresión por cuantiles, se debe proporcionar una métrica personalizada según el cuantil que se esté estimando.

In [35]:
# Función de coste para cada cuantil (pinball_loss)
# ==============================================================================
def mean_pinball_loss_q10(y_true, y_pred):
    """
    Pinball loss for quantile 10.
    """
    return mean_pinball_loss(y_true, y_pred, alpha=0.1)


def mean_pinball_loss_q90(y_true, y_pred):
    """
    Pinball loss for quantile 90.
    """
    return mean_pinball_loss(y_true, y_pred, alpha=0.9)
In [36]:
# 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              = 7,
        initial_train_size = len(data[:end_train]),
        refit              = False,
    )

results_grid_q10 = bayesian_search_forecaster(
                       forecaster     = forecaster_q10,
                       y              = data.loc[:end_validation, 'users'],
                       cv             = cv,
                       metric         = mean_pinball_loss_q10,
                       search_space   = search_space,
                       n_trials       = 10,
                       random_state   = 123,
                       return_best    = True,
                       n_jobs         = 'auto',
                       verbose        = False,
                       show_progress  = True
                   )

results_grid_q90 = bayesian_search_forecaster(
                       forecaster    = forecaster_q90,
                       y             = data.loc[:end_validation, 'users'],
                       cv            = cv,
                       metric        = mean_pinball_loss_q90,
                       search_space  = search_space,
                       n_trials      = 10,
                       random_state  = 123,
                       return_best   = True,
                       n_jobs        = 'auto',
                       verbose       = False,
                       show_progress = True
                   )
`Forecaster` refitted using the best-found lags and parameters, and the whole data set: 
  Lags: [1 2 3 4 5 6 7] 
  Parameters: {'n_estimators': 250, 'max_depth': 3, 'learning_rate': 0.04582398297973883}
  Backtesting metric: 222.8916557556898
`Forecaster` refitted using the best-found lags and parameters, and the whole data set: 
  Lags: [1 2 3 4 5 6 7] 
  Parameters: {'n_estimators': 400, 'max_depth': 4, 'learning_rate': 0.02579065805327433}
  Backtesting metric: 164.5817884709446

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

In [37]:
# Backtesting con datos de test
# ==============================================================================
cv = TimeSeriesFold(
        steps              = 7,
        initial_train_size = len(data.loc[:end_validation]),
        refit              = False,
    )
metric_q10, predictions_q10 = backtesting_forecaster(
                                  forecaster    = forecaster_q10,
                                  y             = data['users'],
                                  cv            = cv,
                                  metric        = mean_pinball_loss_q10,
                                  n_jobs        = 'auto',
                                  verbose       = False,
                                  show_progress = True
                              )

metric_q90, predictions_q90 = backtesting_forecaster(
                                  forecaster    = forecaster_q90,
                                  y             = data['users'],
                                  cv            = cv,
                                  metric        = mean_pinball_loss_q90,
                                  n_jobs        = 'auto',
                                  verbose       = False,
                                  show_progress = True
                              )
In [38]:
# 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_q90.index, y=predictions_q90['pred'],
        mode='lines', marker=dict(color="#444"), line=dict(width=0), showlegend=False
    ),
    go.Scatter(
        name='Lower Bound', x=predictions_q10.index, y=predictions_q10['pred'],
        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="Valores reales vs predicciones en los datos de test",
    xaxis_title="Date time",
    yaxis_title="users",
    width=750,
    height=350,
    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()
In [39]:
# Cobertura del intervalo en los datos de test
# ==============================================================================
coverage = empirical_coverage(
                y           = data.loc[end_validation:, 'users'],
                lower_bound = predictions_q10["pred"], 
                upper_bound = predictions_q90["pred"]
            )
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: 49.53 %
Area del intervalo: 207151.73

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

Información de sesión

In [40]:
import session_info
session_info.show(html=False)
-----
lightgbm            4.4.0
matplotlib          3.9.0
numpy               2.0.2
optuna              4.0.0
pandas              2.2.3
plotly              5.23.0
scipy               1.14.1
session_info        1.0.0
skforecast          0.14.0
sklearn             1.5.2
statsmodels         0.14.2
-----
IPython             8.25.0
jupyter_client      8.6.2
jupyter_core        5.7.2
notebook            6.4.12
-----
Python 3.12.4 | packaged by Anaconda, Inc. | (main, Jun 18 2024, 15:03:56) [MSC v.1929 64 bit (AMD64)]
Windows-11-10.0.26100-SP0
-----
Session information updated at 2024-11-04 11:52

Bibliografía

Hyndman, R.J., & Athanasopoulos, G. (2021) Forecasting: principles and practice, 3rd edition, OTexts: Melbourne, Australia. book

Time Series Analysis and Forecasting with ADAM Ivan Svetunkov book

Python for Finance: Mastering Data-Driven Finance book

Forecasting: theory and practice PDF

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 en tu investigación o publicación, te lo agradeceríamos mucho que lo cites. ¡Muchas gracias!

Zenodo:

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

APA:

Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast (Version 0.14.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.14.0}, month = {11}, year = {2024}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382788} }


¿Te ha gustado el artículo? Tu ayuda es importante

Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊


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.

  • NoComercial: No puedes utilizar el material para fines comerciales.

  • CompartirIgual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.