Benchmarking 30 statistical/Machine Learning models on the VN1 Forecasting — Accuracy challenge

This article was first published on T. Moudiki's Webpage - Python , and kindly contributed to python-bloggers. (You can report issue about the content on this page here)
Want to share your content on python-bloggers? click here.

This post is about the VN1 Forecasting – Accuracy challenge. The aim is to accurately forecast future sales for various products across different
clients and warehouses, using historical sales and pricing data.

Phase 1 was a warmup to get an idea of what works and what wouldn’t (and… for overfitting the validation set, so that the leaderboard is almost meaningless). It’s safe to say, based on empirical observations, that an advanced artillery would be useless here. In phase 2 (people are still welcome to enter the challenge, no pre-requisites from phase 1 needed), the validation set is provided, and there’s no leaderboard for the test set (which is great, basically: “real life”; no overfitting).

My definition of “winning” the challenge will be to have an accuracy close to the winning solution by a factor of 1% (2 decimals). Indeed, the focus on accuracy means: we are litterally targetting a point on the real line (well, the “real line”, the interval is probably bounded but still contains an infinite number of points). If the metric was a metric for quantifying uncertainty… there would be too much winners 🙂

In the examples, I show you how you can start the competition by benchmarking 30 statistical/Machine Learning models on a few products, based on the validation set provided yesterday. No tuning, no overfitting. Only hold-out set validation. You can notice, on some examples, that a model can be the most accurate on point forecasting, but completely off-track when trying to capture the uncertainty aroung the point forecast. Food for thought.

0 – Functions and packages

!pip uninstall nnetsauce --yes
!pip install nnetsauce --upgrade --no-cache-dir
import numpy as np
import pandas as pd
def rm_leading_zeros(df):
    if 'y' in df.columns and (df['y'] == 0).any():
        first_non_zero_index_y = (df['y'] != 0).idxmax()
        df = df.loc[first_non_zero_index_y:].reset_index(drop=True)
    return df.dropna().reset_index(drop=True)
# Read price data
price = pd.read_csv("/kaggle/input/2024-10-02-vn1-forecasting/Phase 0 - Price.csv", na_values=np.nan)
price["Value"] = "Price"
price = price.set_index(["Client", "Warehouse","Product", "Value"]).stack()

# Read sales data
sales = pd.read_csv("/kaggle/input/2024-10-02-vn1-forecasting/Phase 0 - Sales.csv", na_values=np.nan)
sales["Value"] = "Sales"
sales = sales.set_index(["Client", "Warehouse","Product", "Value"]).stack()

# Read price validation data
price_test = pd.read_csv("/kaggle/input/2024-10-02-vn1-forecasting/Phase 1 - Price.csv", na_values=np.nan)
price_test["Value"] = "Price"
price_test = price_test.set_index(["Client", "Warehouse","Product", "Value"]).stack()

# Read sales validation data
sales_test = pd.read_csv("/kaggle/input/2024-10-02-vn1-forecasting/Phase 1 - Sales.csv", na_values=np.nan)
sales_test["Value"] = "Sales"
sales_test = sales_test.set_index(["Client", "Warehouse","Product", "Value"]).stack()

# Create single dataframe
df = pd.concat([price, sales]).unstack("Value").reset_index()
df.columns = ["Client", "Warehouse", "Product", "ds", "Price", "y"]
df["ds"] = pd.to_datetime(df["ds"])
df = df.astype({"Price": np.float32,
                "y": np.float32,
                "Client": "category",
                "Warehouse": "category",
                "Product": "category",
                })

df_test = pd.concat([price_test, sales_test]).unstack("Value").reset_index()
df_test.columns = ["Client", "Warehouse", "Product", "ds", "Price", "y"]
df_test["ds"] = pd.to_datetime(df_test["ds"])
df_test = df_test.astype({"Price": np.float32,
                "y": np.float32,
                "Client": "category",
                "Warehouse": "category",
                "Product": "category",
                })
display(df.head())
display(df_test.head())
ClientWarehouseProductdsPricey
0013672020-07-0610.907.00
1013672020-07-1310.907.00
2013672020-07-2010.907.00
3013672020-07-2715.587.00
4013672020-08-0327.297.00
ClientWarehouseProductdsPricey
0013672023-10-0951.861.00
1013672023-10-1651.861.00
2013672023-10-2351.861.00
3013672023-10-3051.232.00
4013672023-11-0651.231.00
df.describe()
df_test.describe()
dsPricey
count19568985630.00195689.00
mean2023-11-20 00:00:0063.4319.96
min2023-10-09 00:00:000.000.00
25%2023-10-30 00:00:0017.970.00
50%2023-11-20 00:00:0028.000.00
75%2023-12-11 00:00:0048.275.00
max2024-01-01 00:00:005916.0415236.00
stdNaN210.48128.98
display(df.info())
display(df_test.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2559010 entries, 0 to 2559009
Data columns (total 6 columns):
 #   Column     Dtype         
---  ------     -----         
 0   Client     category      
 1   Warehouse  category      
 2   Product    category      
 3   ds         datetime64[ns]
 4   Price      float32       
 5   y          float32       
dtypes: category(3), datetime64[ns](1), float32(2)
memory usage: 51.6 MB



None


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 195689 entries, 0 to 195688
Data columns (total 6 columns):
 #   Column     Non-Null Count   Dtype         
---  ------     --------------   -----         
 0   Client     195689 non-null  category      
 1   Warehouse  195689 non-null  category      
 2   Product    195689 non-null  category      
 3   ds         195689 non-null  datetime64[ns]
 4   Price      85630 non-null   float32       
 5   y          195689 non-null  float32       
dtypes: category(3), datetime64[ns](1), float32(2)
memory usage: 4.3 MB



None

1 – AutoML for a few products

1 – 1 Select a product

np.random.seed(413)
#np.random.seed(13) # uncomment to select a different product
#np.random.seed(1413) # uncomment to select a different product
#np.random.seed(71413) # uncomment to select a different product
random_series = df.sample(1).loc[:, ['Client', 'Warehouse', 'Product']]
client = random_series.iloc[0]['Client']
warehouse = random_series.iloc[0]['Warehouse']
product = random_series.iloc[0]['Product']
df_filtered = df[(df.Client == client) & (df.Warehouse == warehouse) & (df.Product == product)]
df_filtered = rm_leading_zeros(df_filtered)
display(df_filtered)
df_filtered_test = df_test[(df_test.Client == client) & (df_test.Warehouse == warehouse) & (df_test.Product == product)]
display(df_filtered_test)
ClientWarehouseProductdsPricey
0418884982021-11-1554.951.00
1418884982021-11-2254.955.00
2418884982021-11-2954.959.00
3418884982021-12-0654.9520.00
4418884982021-12-1354.9511.00
5418884982021-12-2054.958.00
6418884982021-12-2754.9513.00
7418884982022-01-0354.9513.00
8418884982022-01-1054.9513.00
9418884982022-01-1752.8426.00
10418884982022-01-2454.9519.00
11418884982022-01-3149.4510.00
12418884982022-02-0754.9515.00
13418884982022-02-1454.9510.00
14418884982022-02-2154.9510.00
15418884982022-02-2854.9517.00
16418884982022-03-0754.9531.00
17418884982022-03-1454.9520.00
18418884982022-03-2154.951.00
19418884982022-03-2854.951.00
20418884982022-04-1854.951.00
21418884982022-12-1254.954.00
22418884982022-12-1954.952.00
23418884982022-12-2654.955.00
24418884982023-01-0254.2616.00
25418884982023-01-0954.957.00
26418884982023-01-1654.954.00
27418884982023-01-2354.957.00
28418884982023-01-3054.957.00
29418884982023-02-0654.957.00
30418884982023-02-1354.959.00
31418884982023-02-2054.956.00
32418884982023-02-2754.9518.00
33418884982023-03-0654.9510.00
34418884982023-03-1354.9514.00
35418884982023-03-2054.6418.00
36418884982023-03-2754.9513.00
37418884982023-04-0354.4321.00
38418884982023-04-1054.4924.00
39418884982023-04-1754.9512.00
40418884982023-04-2454.958.00
41418884982023-05-0154.9512.00
42418884982023-05-0854.4511.00
43418884982023-05-1554.956.00
44418884982023-06-2654.9526.00
45418884982023-07-0354.9521.00
46418884982023-07-1047.3729.00
47418884982023-07-1754.9510.00
48418884982023-07-2454.9515.00
49418884982023-07-3154.9517.00
50418884982023-08-0754.9510.00
51418884982023-08-1454.9518.00
52418884982023-08-2154.955.00
53418884982023-09-0443.961.00
54418884982023-09-1154.952.00
55418884982023-09-1853.739.00
56418884982023-09-2551.296.00
57418884982023-10-0254.958.00
ClientWarehouseProductdsPricey
174213418884982023-10-0954.9510.00
174214418884982023-10-1654.4511.00
174215418884982023-10-2354.958.00
174216418884982023-10-3054.9515.00
174217418884982023-11-0654.9513.00
174218418884982023-11-1354.0312.00
174219418884982023-11-2054.9511.00
174220418884982023-11-2754.9515.00
174221418884982023-12-0454.9511.00
174222418884982023-12-11NaN0.00
174223418884982023-12-18NaN0.00
174224418884982023-12-25NaN0.00
174225418884982024-01-01NaN0.00
df_selected = df_filtered[['y', 'ds']].set_index('ds')
df_selected.index = pd.to_datetime(df_selected.index)
display(df_selected)
y
ds
2021-11-151.00
2021-11-225.00
2021-11-299.00
2021-12-0620.00
2021-12-1311.00
2021-12-208.00
2021-12-2713.00
2022-01-0313.00
2022-01-1013.00
2022-01-1726.00
2022-01-2419.00
2022-01-3110.00
2022-02-0715.00
2022-02-1410.00
2022-02-2110.00
2022-02-2817.00
2022-03-0731.00
2022-03-1420.00
2022-03-211.00
2022-03-281.00
2022-04-181.00
2022-12-124.00
2022-12-192.00
2022-12-265.00
2023-01-0216.00
2023-01-097.00
2023-01-164.00
2023-01-237.00
2023-01-307.00
2023-02-067.00
2023-02-139.00
2023-02-206.00
2023-02-2718.00
2023-03-0610.00
2023-03-1314.00
2023-03-2018.00
2023-03-2713.00
2023-04-0321.00
2023-04-1024.00
2023-04-1712.00
2023-04-248.00
2023-05-0112.00
2023-05-0811.00
2023-05-156.00
2023-06-2626.00
2023-07-0321.00
2023-07-1029.00
2023-07-1710.00
2023-07-2415.00
2023-07-3117.00
2023-08-0710.00
2023-08-1418.00
2023-08-215.00
2023-09-041.00
2023-09-112.00
2023-09-189.00
2023-09-256.00
2023-10-028.00
df_selected_test = df_filtered_test[['y', 'ds']].set_index('ds')
df_selected_test.index = pd.to_datetime(df_selected_test.index)
display(df_selected_test)
y
ds
2023-10-0910.00
2023-10-1611.00
2023-10-238.00
2023-10-3015.00
2023-11-0613.00
2023-11-1312.00
2023-11-2011.00
2023-11-2715.00
2023-12-0411.00
2023-12-110.00
2023-12-180.00
2023-12-250.00
2024-01-010.00

1 – 2 AutoML (Hold-out set)

import nnetsauce as ns
import numpy as np
from time import time
# Custom error metric 
def custom_error(objective, submission):
    try: 
        pred = submission.mean.values.ravel()
        true = objective.values.ravel()
        abs_err = np.nansum(np.abs(pred - true))
        err = np.nansum((pred - true))
        score = abs_err + abs(err)
        score /= true.sum().sum()
    except Exception:
        score = 1000
    return score
regr_mts = ns.LazyMTS(verbose=0, ignore_warnings=True, 
                          custom_metric=custom_error,                      
                          type_pi = "scp2-kde", # sequential split conformal prediction
                          lags = 1, n_hidden_features = 0,
                          sort_by = "Custom metric",
                          replications=250, kernel="tophat",
                          show_progress=False, preprocess=False)
models, predictions = regr_mts.fit(X_train=df_selected.values.ravel(), 
                                   X_test=df_selected_test.values.ravel())

100%|██████████| 32/32 [00:24<00:00,  1.28it/s]

1 – 3 models leaderboard

display(models)
RMSEMAEMPLWINKLERSCORECOVERAGETime TakenCustom metric
Model
MTS(RANSACRegressor)5.855.202.6030.43100.000.830.65
ETS5.835.452.7227.99100.000.020.81
MTS(TweedieRegressor)6.184.502.2532.86100.000.780.83
MTS(LassoLars)6.234.482.2434.33100.000.790.83
MTS(Lasso)6.234.482.2434.33100.000.780.83
MTS(RandomForestRegressor)5.695.152.5838.53100.001.100.84
MTS(ElasticNet)6.244.472.2432.58100.000.780.84
MTS(DummyRegressor)6.204.492.2532.97100.000.790.85
MTS(HuberRegressor)5.914.462.2329.4192.310.800.86
ARIMA6.674.762.3828.68100.000.041.01
MTS(BayesianRidge)6.874.852.4233.29100.000.791.04
MTS(PassiveAggressiveRegressor)7.615.872.9442.1284.620.791.06
MTS(LinearSVR)7.234.822.4134.22100.001.021.06
MTS(DecisionTreeRegressor)8.667.283.6457.1192.310.811.14
MTS(RidgeCV)7.555.802.9034.9592.310.771.16
MTS(Ridge)7.645.862.9338.0884.620.801.17
MTS(ElasticNetCV)7.345.772.8932.6184.620.881.17
MTS(TransformedTargetRegressor)7.746.033.0234.5784.620.771.19
MTS(LinearRegression)7.746.033.0234.5784.620.811.19
MTS(Lars)7.746.033.0234.5784.620.801.19
MTS(MLPRegressor)7.585.142.5731.3292.311.331.19
MTS(LassoLarsIC)7.776.033.0236.1984.620.781.20
MTS(LassoCV)7.776.033.0238.9284.620.901.20
MTS(LarsCV)7.796.053.0239.0984.620.791.21
MTS(LassoLarsCV)7.796.053.0239.0984.620.791.21
MTS(SGDRegressor)7.846.043.0240.5284.620.801.22
MTS(KNeighborsRegressor)8.005.652.8232.97100.000.801.35
MTS(AdaBoostRegressor)8.616.353.1837.34100.000.971.56
MTS(ExtraTreesRegressor)11.479.264.6355.9684.621.032.12
MTS(ExtraTreeRegressor)13.7210.885.4485.4584.620.792.52
MTS(BaggingRegressor)15.4913.106.5595.7076.920.993.21

2 – Best model

best_model = regr_mts.get_best_model()
display(best_model)
DeepMTS(kernel='tophat', n_hidden_features=0, n_layers=1,
        obj=RANSACRegressor(random_state=42), replications=250,
        show_progress=False, type_pi='scp2-kde')

In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.