Reducing the influence of Covid-19 on time series forecasting models using skforecast

If you like  Skforecast ,  please give us a star on   GitHub! ⭐️

Reducing the influence of Covid-19 on time series forecasting models using skforecast

Joaquin Amat Rodrigo, Javier Escobar Ortiz
December, 2022

More about forecasting: Skforecast examples

Weighted time series forecasting


Although historical data is available in many real use cases of forecasting, not all are reliable. Some examples of these scenarios are:

  • IoT sensors: within the Internet of Things, sensors capture the raw data from the physical world. Often the sensors are deployed or installed in harsh environments. This inevitably means that the sensors are prone to failure, malfunction, and rapid attrition, causing the sensor to produce unusual and erroneous readings.

  • Factory shutdown: every certain period of operation, factories need to be shut down for repair, overhaul, or maintenance activities. These events cause production to stop, generating a gap in the data.

  • Pandemic (Covid-19): the Covid 19 pandemic changed population behavior significantly, directly impacting many time series such as production, sales, and transportation.

The presence of unreliable or unrepresentative values in the data history is a major problem, as it hinders model learning. For most forecasting algorithms, removing that part of the data is not an option because they require the time series to be complete. An alternative solution is to reduce the weight of the affected observations during model training. This document shows two examples of how skforecast makes it easy to apply this strategy.

  Note

In the following examples, a portion of the time series is excluded from model training by giving it a weight of zero. However, the use of weights is not limited to including or excluding observations, but to balancing the degree of influence of each observation in the forecasting model. For example, an observation with a weight of 10 has 10 times more impact on the model training than an observation with a weight of 1.

  Warning

In most gradient boosting implementations (LightGBM, XGBoost, CatBoost), samples with zero weight are ignored when calculating the gradients and hessians. However, the values for those samples are still considered when building the feature histograms. Therefore, the resulting model may differ from the model trained without the zero-weighted samples. See more details in this issue.

Libraries

In [2]:
# Libraries
# ==============================================================================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
plt.rcParams['lines.linewidth'] = 1.5
dark_style = {
    'figure.facecolor': '#212946',
    'axes.facecolor': '#212946',
    'savefig.facecolor':'#212946',
    'axes.grid': True,
    'axes.grid.which': 'both',
    'axes.spines.left': False,
    'axes.spines.right': False,
    'axes.spines.top': False,
    'axes.spines.bottom': False,
    'grid.color': '#2A3459',
    'grid.linewidth': '1',
    'text.color': '0.9',
    'axes.labelcolor': '0.9',
    'xtick.color': '0.9',
    'ytick.color': '0.9',
    'font.size': 12
}
plt.rcParams.update(dark_style)
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from skforecast.ForecasterAutoreg import ForecasterAutoreg
from skforecast.model_selection import backtesting_forecaster

Covid-19 lockdown

During the lockdown period imposed as a consequence of the covid-19 pandemic, the behavior of the population was altered. An example of this can be seen in the use of the bicycle rental service in the city of Madrid (Spain).

Data

In [3]:
# Data download
# ==============================================================================
url = (
    'https://raw.githubusercontent.com/JoaquinAmatRodrigo/'
    'Estadistica-machine-learning-python/master/data/usuarios_diarios_bicimad.csv'
)
data = pd.read_csv(url, sep=',')

# Data preprocessing
# ==============================================================================
data['fecha'] = pd.to_datetime(data['fecha'], format='%Y-%m-%d')
data = data[['fecha', 'Usos bicis total día']]
data.columns = ['date', 'users']
data = data.set_index('date')
data = data.asfreq('D')
data = data.sort_index()
In [4]:
# Split data into train-val-test
# ==============================================================================
data = data.loc['2020-01-01': '2021-12-31']
end_train = '2021-06-01'
data_train = data.loc[: end_train, :]
data_test  = data.loc[end_train:, :]

print(f"Dates train : {data_train.index.min()} --- {data_train.index.max()}  (n={len(data_train)})")
print(f"Dates test  : {data_test.index.min()} --- {data_test.index.max()}  (n={len(data_test)})")
Dates train : 2020-01-01 00:00:00 --- 2021-06-01 00:00:00  (n=518)
Dates test  : 2021-06-01 00:00:00 --- 2021-12-31 00:00:00  (n=214)
In [5]:
# Time series plot
# ==============================================================================
fig, ax = plt.subplots(figsize=(12, 4))
data_train.users.plot(ax=ax, label='train', linewidth=1)
data_test.users.plot(ax=ax, label='test', linewidth=1)
ax.axvspan(
    pd.to_datetime('2020-03-16'),
    pd.to_datetime('2020-04-21'), 
    label="Covid-19 confinement",
    color="red",
    alpha=0.3
)

ax.axvspan(
    pd.to_datetime('2020-04-21'),
    pd.to_datetime('2020-05-31'), 
    label="Recovery time",
    color="white",
    alpha=0.3
)

ax.set_title('Number of users BiciMAD')
ax.legend();

Include the whole time series

A forecaster is initialized without taking into consideration the lockdown period.

In [7]:
# Create recursive multi-step forecaster (ForecasterAutoreg)
# ==============================================================================
forecaster = ForecasterAutoreg(
                 regressor = Ridge(),
                 lags      = 21,
             )
             
forecaster
Out[7]:
================= 
ForecasterAutoreg 
================= 
Regressor: Ridge() 
Lags: [ 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21] 
Transformer for y: None 
Transformer for exog: None 
Window size: 21 
Weight function included: False 
Exogenous included: False 
Type of exogenous variable: None 
Exogenous variables names: None 
Training range: None 
Training index type: None 
Training index frequency: None 
Regressor parameters: {'alpha': 1.0, 'copy_X': True, 'fit_intercept': True, 'max_iter': None, 'normalize': 'deprecated', 'positive': False, 'random_state': None, 'solver': 'auto', 'tol': 0.001} 
Creation date: 2022-11-18 17:27:52 
Last fit date: None 
Skforecast version: 0.6.0 
Python version: 3.8.13 

Once the model is created, a backtesting process is run to simulate the behavior of the forecaster if it had predicted the test set in 10-day batches.

In [8]:
# Backtesting: predict next 7 days at a time.
# ==============================================================================
metric, predictions_backtest = backtesting_forecaster(
                                   forecaster         = forecaster,
                                   y                  = data.users,
                                   initial_train_size = len(data.loc[:end_train]),
                                   fixed_train_size   = False,
                                   steps              = 7,
                                   metric             = 'mean_absolute_error',
                                   refit              = False,
                                   verbose            = False
                               )

print(f"Backtest error: {metric:.1f}")
Backtest error: 1469.1

Exclude part of the time series

To minimize the influence on the model of these dates, a custom function is created weights following the rules:

  • Weight of 0 if index date is:

    • Within the lockdown period (2020-03-16 to 2020-04-21).

    • Within the recovery period (2020-04-21 to 2020-05-31).

    • 21 days after the recovery period to avoid including impacted values as lags (2020-05-31) to 2020-06-21).

  • Weight of 1 otherwise.

If an observation has a weight of 0, it has no influence during model training.

In [9]:
# Custom function to create weights
# ==============================================================================
def custom_weights(index):
    """
    Return 0 if index is one between 2020-03-16 and 2020-06-21.
    """
    weights = np.where((index >= '2020-03-16') & (index <= '2020-06-21'), 0, 1)
    
    return weights

Again, a ForecasterAutoreg is initialized but this time including the custom_weights function.

In [11]:
# Create recursive multi-step forecaster (ForecasterAutoreg)
# ==============================================================================
forecaster = ForecasterAutoreg(
                 regressor   = Ridge(random_state=123),
                 lags        = 21,
                 weight_func = custom_weights
             )

# Backtesting: predict next 7 days at a time.
# ==============================================================================
metric, predictions_backtest = backtesting_forecaster(
                                   forecaster         = forecaster,
                                   y                  = data.users,
                                   initial_train_size = len(data.loc[:end_train]),
                                   fixed_train_size   = False,
                                   steps              = 7,
                                   metric             = 'mean_absolute_error',
                                   refit              = False,
                                   verbose            = False
                               )

print(f"Backtest error: {metric:.1f}")
Backtest error: 1404.7

Giving a weight of 0 to the lockdown period (excluding it from the model training) slightly improves the forecasting performance.

Power plant shutdown


Power plants used to generate energy are very complex installations that require a high level of maintenance. It is common that, every certain period of operation, the plant has to be shut down for repair, overhaul, or maintenance activities.

Data

In [12]:
# Data download
# ==============================================================================
url = ('https://raw.githubusercontent.com/JoaquinAmatRodrigo/skforecast/master/'
       'data/energy_production_shutdown.csv')
data = pd.read_csv(url, sep=',')

# Data preprocessing
# ==============================================================================
data['date'] = pd.to_datetime(data['date'], format='%Y-%m-%d')
data = data.set_index('date')
data = data.asfreq('D')
data = data.sort_index()
data.head()
Out[12]:
production
date
2012-01-01 375.1
2012-01-02 474.5
2012-01-03 573.9
2012-01-04 539.5
2012-01-05 445.4
In [13]:
# Split data into train-test
# ==============================================================================
data = data.loc['2012-01-01 00:00:00': '2014-12-30 23:00:00']
end_train = '2013-12-31 23:59:00'
data_train = data.loc[: end_train, :]
data_test  = data.loc[end_train:, :]

print(f"Dates train : {data_train.index.min()} --- {data_train.index.max()}  (n={len(data_train)})")
print(f"Dates test  : {data_test.index.min()} --- {data_test.index.max()}  (n={len(data_test)})")
Dates train : 2012-01-01 00:00:00 --- 2013-12-31 00:00:00  (n=731)
Dates test  : 2014-01-01 00:00:00 --- 2014-12-30 00:00:00  (n=364)
In [14]:
# Time series plot
# ==============================================================================
fig, ax = plt.subplots(figsize=(12, 4))
data_train.production.plot(ax=ax, label='train', linewidth=1)
data_test.production.plot(ax=ax, label='test', linewidth=1)
ax.axvspan(
    pd.to_datetime('2012-06-01'),
    pd.to_datetime('2012-09-30'), 
    label="Shutdown",
    color="red",
    alpha=0.1
)
ax.set_title('Energy production')
ax.legend();

Include the whole time series

A forecaster is initialized without taking in consideration the shutdown period.

In [15]:
# Create recursive multi-step forecaster (ForecasterAutoreg)
# ==============================================================================
forecaster = ForecasterAutoreg(
                 regressor = Ridge(random_state=123),
                 lags      = 21,
             )
In [16]:
# Backtesting: predict next 10 days at a time.
# ==============================================================================
metric, predictions_backtest = backtesting_forecaster(
                                   forecaster         = forecaster,
                                   y                  = data.production,
                                   initial_train_size = len(data.loc[:end_train]),
                                   fixed_train_size   = False,
                                   steps              = 10,
                                   metric             = 'mean_absolute_error',
                                   refit              = False,
                                   verbose            = False
                               )

print(f"Backtest error: {metric:.1f}")
Backtest error: 28.4

Exclude part of the time series

The factory shutdown took place from 2012-06-01 to 2012-09-30. To minimize the influence on the model of these dates, a custom function is created that gives a value of 0 if the index date is within the shutdown period or 21 days later (lags used by the model) and 1 otherwise. If an observation has a weight of 0, it has no influence at all during model training.

In [17]:
# Custom function to create weights
# ==============================================================================
def custom_weights(index):
    """
    Return 0 if index is one between 2012-06-01 and 2012-10-21.
    """
    weights = np.where((index >= '2012-06-01') & (index <= '2012-10-21'), 0, 1)
    return weights
In [18]:
# Create recursive multi-step forecaster (ForecasterAutoreg)
# ==============================================================================
forecaster = ForecasterAutoreg(
                 regressor   = Ridge(random_state=123),
                 lags        = 21,
                 weight_func = custom_weights
             )

# Backtesting: predict next 10 days at a time.
# ==============================================================================
metric, predictions_backtest = backtesting_forecaster(
                                   forecaster         = forecaster,
                                   y                  = data.production,
                                   initial_train_size = len(data.loc[:end_train]),
                                   fixed_train_size   = False,
                                   steps              = 12,
                                   metric             = 'mean_absolute_error',
                                   refit              = False,
                                   verbose            = False
                               )

print(f"Backtest error: {metric:.1f}")
Backtest error: 26.8

As in the previous example, excluding the observations during the shutdown period slightly improves the forecasting performance.

Session information

In [19]:
import session_info
session_info.show(html=False)
-----
matplotlib          3.5.0
numpy               1.23.0
pandas              1.4.0
session_info        1.0.0
skforecast          0.6.0
sklearn             1.1.0
-----
IPython             8.5.0
jupyter_client      7.3.5
jupyter_core        4.11.1
notebook            6.4.12
-----
Python 3.8.13 (default, Mar 28 2022, 11:38:47) [GCC 7.5.0]
Linux-5.15.0-1023-aws-x86_64-with-glibc2.17
-----
Session information updated at 2022-11-18 17:30

¿How to cite this document?

Reducing the influence of Covid-19 on time series forecasting models using skforecast by Joaquín Amat Rodrigo and Javier Escobar Ortiz, available under a CC BY-NC-SA 4.0 at https://www.cienciadedatos.net/documentos/py45-weighted-time-series-forecasting.html DOI


Did you like the article? Your support is important

Website maintenance has high cost, your contribution will help me to continue generating free educational content. Many thanks! 😊


Creative Commons Licence
This work by Joaquín Amat Rodrigo and Javier Escobar Ortiz is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International.