PyCaret Time Series – Passenger Forecast Dublin Airport¶

Roberto Tavares de Oliveira Neto | Student ID: 2025444¶

Module: Strategic Thinking – Higher Diploma in Data Analytics
CCT College Dublin | 2025–2026

Use of AI Tools: I have not used any AI tools or technologies in the preparation of this assessment.

Overview¶

Two experimental setups are compared:

  • Setup A (exp1) – Full series including COVID period (reference only)
  • Setup B (exp2) – COVID period excluded, drift imputation (primary setup)

Pipeline per setup:

  1. check_stats – stationarity, seasonality and white noise validation
  2. compare_models – automated benchmarking
  3. create_model – train selected models
  4. tune_model – hyperparameter optimisation (native TS models only)
  5. blend_models – ensemble of top models
  6. stack_models – stacked ensemble with meta-learner
  7. plot_model – decomposition, ACF, diagnostics, in-sample, forecast with CI
  8. finalize_model + predict_model – forecasts stored in dictionary for comparison
  9. get_config('dataset_transformed') – inspect drift imputation applied to COVID gap

Environment Configuration¶

Component Version
Python 3.9.6 (Clang 21.0.0, macOS-26.4.1-arm64)
PyCaret 3.3.2
sktime 0.26.0
scikit-learn 1.4.2
statsmodels 0.14.6
pandas 2.1.4
numpy 1.26.4
jupyter_core 5.8.1
ipykernel 6.30.1

Note: Results may vary on different environments. PyCaret 3.3.2 with sktime 0.26.0 is the validated configuration for this notebook.


1. Libraries and Data Loading¶

In [1]:
import subprocess
import sys

# Install pyjstat if not available
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pyjstat', '-q'])

import warnings
warnings.filterwarnings('ignore')

import pandas as pd
import numpy as np
from collections import OrderedDict
import matplotlib.pyplot as plt
import requests
from pyjstat import pyjstat
WARNING: You are using pip version 21.2.4; however, version 26.0.1 is available.
You should consider upgrading via the '/Library/Developer/CommandLineTools/usr/bin/python3 -m pip install --upgrade pip' command.
In [2]:
url = "https://ws.cso.ie/public/api.restful/PxStat.Data.Cube_API.ReadDataset/ASM01/JSON-stat/2.0/en"
r = requests.get(url)
r.raise_for_status()
json_stat_obj = r.json(object_pairs_hook=OrderedDict)
df_list = pyjstat.from_json_stat(json_stat_obj)
df = pd.DataFrame(df_list[0])

# delete non necessary columns and filter for air arrivals only
df = df.drop(columns=['STATISTIC'])
df = df[df['Mode'] == 'Air'].drop(columns=['Mode'])
df['Date'] = pd.to_datetime(df['Month'])
df = df.set_index('Date').sort_index().drop(columns=['Month'])

# fix to Dublin airport arrivals only (83% of total air arrivals)
df['Dublin_airport'] = df['value'] * 0.83
df = df.drop(columns=['value'])
df = df[df['Direction'] == 'Arrivals'].drop(columns=['Direction'])

print(f"Dataset: {df.shape[0]} observations | {df.index[0].date()} to {df.index[-1].date()}")
df.tail(14)
Dataset: 195 observations | 2010-01-01 to 2026-03-01
Out[2]:
Dublin_airport
Date
2025-02-01 1059.578
2025-03-01 1286.998
2025-04-01 1519.066
2025-05-01 1657.095
2025-06-01 1720.507
2025-07-01 1877.294
2025-08-01 1924.106
2025-09-01 1661.162
2025-10-01 1593.932
2025-11-01 1279.279
2025-12-01 1276.540
2026-01-01 1197.109
2026-02-01 1152.538
2026-03-01 1402.866

2. Setup A – Full Series Including COVID¶

Reference only. Included to demonstrate the impact of the pandemic structural break on model performance.

In [3]:
from pycaret.time_series import *

df_A = df.copy()
df_A.index = df_A.index.to_period('M')
print(f"Setup A: {df_A.shape[0]} observations")
df_A
Setup A: 195 observations
Out[3]:
Dublin_airport
Date
2010-01 604.572
2010-02 628.476
2010-03 752.478
2010-04 610.714
2010-05 803.855
... ...
2025-11 1279.279
2025-12 1276.540
2026-01 1197.109
2026-02 1152.538
2026-03 1402.866

195 rows × 1 columns

In [4]:
exp1 = TSForecastingExperiment()
exp1.setup(
    data=df_A,
    target='Dublin_airport',
    session_id=100,
    fh=12,
    fold=3,
    fold_strategy='expanding',
    seasonal_period=12,
    coverage=0.90,
    verbose=True
)
  Description Value
0 session_id 100
1 Target Dublin_airport
2 Approach Univariate
3 Exogenous Variables Not Present
4 Original data shape (195, 1)
5 Transformed data shape (195, 1)
6 Transformed train set shape (183, 1)
7 Transformed test set shape (12, 1)
8 Rows with missing values 0.0%
9 Fold Generator ExpandingWindowSplitter
10 Fold Number 3
11 Enforce Prediction Interval False
12 Splits used for hyperparameters all
13 User Defined Seasonal Period(s) 12
14 Ignore Seasonality Test False
15 Seasonality Detection Algo user_defined
16 Max Period to Consider 60
17 Seasonal Period(s) Tested [12]
18 Significant Seasonal Period(s) [12]
19 Significant Seasonal Period(s) without Harmonics [12]
20 Remove Harmonics False
21 Harmonics Order Method harmonic_max
22 Num Seasonalities to Use 1
23 All Seasonalities to Use [12]
24 Primary Seasonality 12
25 Seasonality Present True
26 Seasonality Type mul
27 Target Strictly Positive True
28 Target White Noise No
29 Recommended d 1
30 Recommended Seasonal D 0
31 Preprocess False
32 CPU Jobs -1
33 Use GPU False
34 Log Experiment False
35 Experiment Name ts-default-name
36 USI 937f
Out[4]:
<pycaret.time_series.forecasting.oop.TSForecastingExperiment at 0x12acc8700>

2.1 Statistical Validation (Setup A)¶

check_stats() runs formal tests for stationarity (ADF, KPSS), seasonality, and white noise before any model is trained.

In [5]:
exp1.check_stats()
Out[5]:
Test Test Name Data Property Setting Value
0 Summary Statistics Transformed Length 195.0
1 Summary Statistics Transformed # Missing Values 0.0
2 Summary Statistics Transformed Mean 1005.049554
3 Summary Statistics Transformed Median 1026.71
4 Summary Statistics Transformed Standard Deviation 416.164152
5 Summary Statistics Transformed Variance 173192.601164
6 Summary Statistics Transformed Kurtosis 0.025503
7 Summary Statistics Transformed Skewness -0.394332
8 Summary Statistics Transformed # Distinct Values 194.0
9 White Noise Ljung-Box Transformed Test Statictic {'alpha': 0.05, 'K': 24} 865.945938
10 White Noise Ljung-Box Transformed Test Statictic {'alpha': 0.05, 'K': 48} 1208.888895
11 White Noise Ljung-Box Transformed p-value {'alpha': 0.05, 'K': 24} 0.0
12 White Noise Ljung-Box Transformed p-value {'alpha': 0.05, 'K': 48} 0.0
13 White Noise Ljung-Box Transformed White Noise {'alpha': 0.05, 'K': 24} 0.0
14 White Noise Ljung-Box Transformed White Noise {'alpha': 0.05, 'K': 48} 0.0
15 Stationarity ADF Transformed Stationarity {'alpha': 0.05} True
16 Stationarity ADF Transformed p-value {'alpha': 0.05} 0.028922
17 Stationarity ADF Transformed Test Statistic {'alpha': 0.05} -3.069149
18 Stationarity ADF Transformed Critical Value 1% {'alpha': 0.05} -3.467211
19 Stationarity ADF Transformed Critical Value 5% {'alpha': 0.05} -2.877735
20 Stationarity ADF Transformed Critical Value 10% {'alpha': 0.05} -2.575403
21 Stationarity KPSS Transformed Trend Stationarity {'alpha': 0.05} True
22 Stationarity KPSS Transformed p-value {'alpha': 0.05} 0.081382
23 Stationarity KPSS Transformed Test Statistic {'alpha': 0.05} 0.129054
24 Stationarity KPSS Transformed Critical Value 10% {'alpha': 0.05} 0.119
25 Stationarity KPSS Transformed Critical Value 5% {'alpha': 0.05} 0.146
26 Stationarity KPSS Transformed Critical Value 2.5% {'alpha': 0.05} 0.176
27 Stationarity KPSS Transformed Critical Value 1% {'alpha': 0.05} 0.216
28 Normality Shapiro Transformed Normality {'alpha': 0.05} False
29 Normality Shapiro Transformed p-value {'alpha': 0.05} 0.001567
In [6]:
exp1.plot_model(plot='cv')
exp1.plot_model(plot='train_test_split')
In [7]:
best_A = exp1.compare_models(sort='SMAPE', n_select=3)
results_A = exp1.pull()  # called immediately after compare_models
  Model MASE RMSSE MAE RMSE MAPE SMAPE R2 TT (Sec)
ets ETS 0.5264 0.2817 107.6606 114.5154 0.0862 0.0910 0.4690 0.0267
arima ARIMA 0.5388 0.3325 109.8586 134.3540 0.0865 0.0928 0.0874 0.6033
lasso_cds_dt Lasso w/ Cond. Deseasonalize & Detrending 0.5675 0.3372 128.3293 144.4958 0.0944 0.1008 0.4976 0.1033
lr_cds_dt Linear w/ Cond. Deseasonalize & Detrending 0.5680 0.3374 128.4184 144.5756 0.0945 0.1009 0.4971 0.1300
en_cds_dt Elastic Net w/ Cond. Deseasonalize & Detrending 0.5677 0.3373 128.3709 144.5352 0.0945 0.1009 0.4973 0.1067
ridge_cds_dt Ridge w/ Cond. Deseasonalize & Detrending 0.5680 0.3374 128.4184 144.5756 0.0945 0.1009 0.4971 0.1067
llar_cds_dt Lasso Least Angular Regressor w/ Cond. Deseasonalize & Detrending 0.5676 0.3373 128.3355 144.5035 0.0944 0.1009 0.4975 0.0467
br_cds_dt Bayesian Ridge w/ Cond. Deseasonalize & Detrending 0.5706 0.3423 129.4126 146.7938 0.0945 0.1013 0.4815 0.0467
exp_smooth Exponential Smoothing 0.5910 0.3687 120.1458 148.5408 0.0938 0.1040 -0.1009 0.0233
theta Theta Forecaster 0.6646 0.3769 140.7851 156.0420 0.1084 0.1128 0.3272 0.0100
huber_cds_dt Huber w/ Cond. Deseasonalize & Detrending 0.7032 0.3942 147.4265 163.2344 0.1118 0.1217 0.1222 0.1200
auto_arima Auto ARIMA 0.7463 0.4124 160.0231 172.1743 0.1217 0.1319 0.1977 3.5367
rf_cds_dt Random Forest w/ Cond. Deseasonalize & Detrending 0.8412 0.4471 170.2034 180.9643 0.1351 0.1541 -0.5760 0.1233
ada_cds_dt AdaBoost w/ Cond. Deseasonalize & Detrending 0.9034 0.4952 189.6974 204.8522 0.1454 0.1604 -0.3554 0.0600
et_cds_dt Extra Trees w/ Cond. Deseasonalize & Detrending 0.8680 0.4681 175.3988 189.6681 0.1399 0.1616 -0.7129 0.1167
omp_cds_dt Orthogonal Matching Pursuit w/ Cond. Deseasonalize & Detrending 0.9295 0.5341 199.5970 224.2139 0.1475 0.1646 -0.4640 0.1167
gbr_cds_dt Gradient Boosting w/ Cond. Deseasonalize & Detrending 0.8932 0.5333 183.8293 218.6532 0.1485 0.1714 -0.5899 0.0567
lightgbm_cds_dt Light Gradient Boosting w/ Cond. Deseasonalize & Detrending 1.0435 0.5889 217.8155 242.5443 0.1669 0.1890 -0.7832 0.1033
dt_cds_dt Decision Tree w/ Cond. Deseasonalize & Detrending 1.0174 0.5333 203.5298 214.4032 0.1631 0.1965 -1.5193 0.0467
knn_cds_dt K Neighbors w/ Cond. Deseasonalize & Detrending 1.1000 0.5921 222.3731 239.8312 0.1773 0.2119 -1.6969 0.1033
naive Naive Forecaster 1.2969 0.7693 278.2114 320.5331 0.2005 0.2259 -1.6132 1.1633
stlf STLF 1.2359 0.6751 245.9252 270.0803 0.1947 0.2526 -3.5322 0.6533
polytrend Polynomial Trend Forecaster 1.9047 1.1193 415.1086 470.5673 0.2942 0.3574 -4.0191 0.6100
grand_means Grand Means Forecaster 1.9050 1.1213 420.9558 473.8693 0.2957 0.3584 -3.7682 0.5800
snaive Seasonal Naive Forecaster 1.5853 0.8716 315.3723 348.6414 0.2459 0.3663 -6.7446 0.5867
croston Croston 2.1742 1.2115 455.8357 500.0620 0.3388 0.4433 -6.8855 0.0100

2.2 Create and Tune Models (Setup A)¶

In [8]:
models_A = ['exp_smooth', 'arima', 'ets', 'lasso_cds_dt', 'lr_cds_dt']
tune_A   = ['exp_smooth', 'arima', 'ets', 'lasso_cds_dt', 'lr_cds_dt']  # only native TS models support tune_model

resultados_A   = {}
resultados_t_A = {}

for model_name in models_A:
    print(f'Creating: {model_name}')
    resultados_A[model_name] = exp1.create_model(model_name)

for model_name in tune_A:
    print(f'Tuning: {model_name}')
    resultados_t_A[model_name] = exp1.tune_model(resultados_A[model_name])
Creating: exp_smooth
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.2619 0.7870 241.5628 307.5916 0.1930 0.2261 -2.1239
1 2023-03 0.1382 0.0944 32.8447 41.8083 0.0242 0.0243 0.9654
2 2024-03 0.3728 0.2246 86.0299 96.2226 0.0642 0.0617 0.8558
Mean NaT 0.5910 0.3687 120.1458 148.5408 0.0938 0.1040 -0.1009
SD NaT 0.4840 0.3005 88.5578 114.6388 0.0720 0.0876 1.4312
Creating: arima
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.1545 0.7159 221.0189 279.8213 0.1735 0.1951 -1.5853
1 2023-03 0.2908 0.1765 69.1127 78.1979 0.0559 0.0537 0.8791
2 2024-03 0.1709 0.1052 39.4443 45.0427 0.0302 0.0296 0.9684
Mean NaT 0.5388 0.3325 109.8586 134.3540 0.0865 0.0928 0.0874
SD NaT 0.4382 0.2727 79.5299 103.7477 0.0624 0.0730 1.1833
Creating: ets
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.0770 0.5305 206.1783 207.3455 0.1718 0.1885 -0.4195
1 2023-03 0.1366 0.0942 32.4681 41.7288 0.0239 0.0240 0.9656
2 2024-03 0.3654 0.2205 84.3355 94.4720 0.0629 0.0604 0.8610
Mean NaT 0.5264 0.2817 107.6606 114.5154 0.0862 0.0910 0.4690
SD NaT 0.4004 0.1833 72.8096 69.0822 0.0626 0.0705 0.6297
Creating: lasso_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.3554 0.2036 68.0448 79.5626 0.0569 0.0581 0.7910
1 2023-03 0.8838 0.5246 210.0203 232.4646 0.1494 0.1634 -0.0686
2 2024-03 0.4633 0.2835 106.9229 121.4603 0.0770 0.0810 0.7703
Mean NaT 0.5675 0.3372 128.3293 144.4958 0.0944 0.1008 0.4976
SD NaT 0.2280 0.1365 59.9052 64.5122 0.0397 0.0452 0.4004
Creating: lr_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.3561 0.2039 68.1786 79.6925 0.0570 0.0582 0.7903
1 2023-03 0.8841 0.5247 210.0740 232.5020 0.1494 0.1635 -0.0690
2 2024-03 0.4637 0.2837 107.0026 121.5323 0.0771 0.0811 0.7700
Mean NaT 0.5680 0.3374 128.4184 144.5756 0.0945 0.1009 0.4971
SD NaT 0.2278 0.1364 59.8752 64.4770 0.0397 0.0452 0.4004
Tuning: exp_smooth
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.0578 0.5213 202.5059 203.7724 0.1688 0.1848 -0.3710
1 2023-03 0.1389 0.0920 32.9991 40.7578 0.0242 0.0243 0.9672
2 2024-03 0.3765 0.2264 86.8834 96.9969 0.0647 0.0621 0.8535
Mean NaT 0.5244 0.2799 107.4628 113.8424 0.0859 0.0904 0.4832
SD NaT 0.3895 0.1793 70.7143 67.6080 0.0609 0.0685 0.6058
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    0.4s finished
Tuning: arima
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.6543 0.3536 125.2491 138.1992 0.0989 0.1050 0.3694
1 2023-03 0.3446 0.2075 81.8936 91.9285 0.0638 0.0628 0.8329
2 2024-03 0.4903 0.3403 113.1434 145.7729 0.0947 0.0873 0.6691
Mean NaT 0.4964 0.3004 106.7620 125.3002 0.0858 0.0850 0.6238
SD NaT 0.1265 0.0660 18.2659 23.7991 0.0157 0.0173 0.1919
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    2.0s finished
Tuning: ets
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.0770 0.5305 206.1783 207.3455 0.1718 0.1885 -0.4195
1 2023-03 0.1366 0.0942 32.4681 41.7288 0.0239 0.0240 0.9656
2 2024-03 0.3654 0.2205 84.3355 94.4720 0.0629 0.0604 0.8610
Mean NaT 0.5264 0.2817 107.6606 114.5154 0.0862 0.0910 0.4690
SD NaT 0.4004 0.1833 72.8096 69.0822 0.0626 0.0705 0.6297
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    0.5s finished
Tuning: lasso_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.4612 0.2792 88.2830 109.1143 0.0695 0.0697 0.6069
1 2023-03 0.8838 0.5246 210.0158 232.4614 0.1494 0.1634 -0.0686
2 2024-03 0.3151 0.2279 72.7194 97.6240 0.0543 0.0573 0.8516
Mean NaT 0.5534 0.3439 123.6727 146.3999 0.0911 0.0968 0.4633
SD NaT 0.2412 0.1295 61.3835 61.0352 0.0417 0.0474 0.3892
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    0.8s finished
Tuning: lr_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.3561 0.2039 68.1786 79.6925 0.0570 0.0582 0.7903
1 2023-03 0.8841 0.5247 210.0740 232.5020 0.1494 0.1635 -0.0690
2 2024-03 0.4637 0.2837 107.0026 121.5323 0.0771 0.0811 0.7700
Mean NaT 0.5680 0.3374 128.4184 144.5756 0.0945 0.1009 0.4971
SD NaT 0.2278 0.1364 59.8752 64.4770 0.0397 0.0452 0.4004
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    0.9s finished

2.3 Blend and Stack (Setup A)¶

In [9]:
# Blend – weighted average of the top 3 models from compare_models
blended_A = exp1.blend_models(best_A)
print("Blended model created – Setup A")
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.6292 0.3998 120.4427 156.2658 0.0932 0.1010 0.1937
1 2023-03 0.2302 0.1442 54.6971 63.8840 0.0381 0.0390 0.9193
2 2024-03 0.1205 0.0724 27.8157 31.0275 0.0197 0.0196 0.9850
Mean NaT 0.3266 0.2055 67.6519 83.7258 0.0503 0.0532 0.6993
SD NaT 0.2186 0.1405 38.9085 53.0184 0.0312 0.0347 0.3585
Blended model created – Setup A

2.4 Diagnostics and Plots (Setup A)¶

In [10]:
# Seasonal decomposition and ACF for the best individual model
exp1.plot_model(resultados_t_A['exp_smooth'], plot='decomp')
exp1.plot_model(resultados_t_A['exp_smooth'], plot='acf')
In [11]:
for model_name in tune_A:
    print(f'Diagnostics: {model_name}')
    exp1.plot_model(estimator=resultados_t_A[model_name], plot='diagnostics')
Diagnostics: exp_smooth
Diagnostics: arima
Diagnostics: ets
Diagnostics: lasso_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
Diagnostics: lr_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In [12]:
for model_name in tune_A:
    print(f'In-sample: {model_name}')
    exp1.plot_model(resultados_t_A[model_name], plot='insample')
In-sample: exp_smooth
In-sample: arima
In-sample: ets
In-sample: lasso_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In-sample: lr_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.

2.5 Finalize and Store Forecasts (Setup A)¶

In [13]:
forecasts_A        = {}
resultados_final_A = {}

# stack_models not supported in this PyCaret version for time series
# blend_models is used as the ensemble method
all_models_A = {**resultados_A, 'blend': blended_A}

for model_name, model_obj in all_models_A.items():
    source      = resultados_t_A.get(model_name, model_obj)
    final_model = exp1.finalize_model(source)
    pred        = exp1.predict_model(final_model, fh=12)
    forecasts_A[model_name]        = pred['y_pred']
    resultados_final_A[model_name] = final_model
    print(f'Forecast – {model_name}:')
    display(pred)
    exp1.plot_model(final_model, plot='forecast', data_kwargs={'fh': 12})
    print()
Forecast – exp_smooth:
y_pred
2026-04 1581.6608
2026-05 1720.6196
2026-06 1832.7146
2026-07 2041.7404
2026-08 2069.9425
2026-09 1794.0123
2026-10 1706.0981
2026-11 1370.2635
2026-12 1339.9335
2027-01 1254.7507
2027-02 1254.1143
2027-03 1465.3596
Forecast – arima:
y_pred
2026-04 1549.5826
2026-05 1657.3518
2026-06 1736.2438
2026-07 1882.2799
2026-08 1925.8036
2026-09 1755.0969
2026-10 1700.8654
2026-11 1489.3823
2026-12 1467.4014
2027-01 1409.8464
2027-02 1411.5449
2027-03 1545.9635
Forecast – ets:
y_pred
2026-04 1573.6837
2026-05 1711.4875
2026-06 1822.9991
2026-07 2032.1108
2026-08 2060.7454
2026-09 1784.9221
2026-10 1696.5987
2026-11 1360.3668
2026-12 1326.9046
2027-01 1239.1107
2027-02 1237.3567
2027-03 1443.5386
Forecast – lasso_cds_dt:
y_pred
2026-04 1542.0120
2026-05 1628.6971
2026-06 1704.8745
2026-07 1932.0375
2026-08 2057.1938
2026-09 1793.3397
2026-10 1680.6175
2026-11 1320.3128
2026-12 1285.1271
2027-01 1148.7629
2027-02 1123.8197
2027-03 1268.6165
Forecast – lr_cds_dt:
y_pred
2026-04 1557.6938
2026-05 1640.9832
2026-06 1708.6036
2026-07 1840.1089
2026-08 1832.0089
2026-09 1624.9880
2026-10 1530.8852
2026-11 1288.6661
2026-12 1243.2458
2027-01 1158.4668
2027-02 1160.6390
2027-03 1290.4236
Forecast – blend:
y_pred
2026-04 1586.7699
2026-05 1704.5554
2026-06 1783.6016
2026-07 1947.6311
2026-08 1968.4477
2026-09 1718.2799
2026-10 1633.5293
2026-11 1334.4188
2026-12 1305.8150
2027-01 1220.5191
2027-02 1204.5537
2027-03 1398.7796


3. Setup B – COVID Excluded (Primary Setup)¶

The COVID period (Jan 2020 – Feb 2022) is removed and the index rebuilt with NaN in the gap. PyCaret imputes these using drift interpolation — a linear bridge between pre- and post-pandemic levels that preserves the seasonal structure.

In [14]:
df_B = df[(df.index < '2020-01-01') | (df.index > '2022-02-01')].copy()
df_B = df_B[['Dublin_airport']].dropna()
df_B.index = df_B.index.to_period('M')

idx_full = pd.period_range(min(df_B.index), max(df_B.index), freq='M')
df_B = df_B.reindex(idx_full, fill_value=np.nan)

print(f"Setup B: {df_B.shape[0]} observations")
print(f"  Period   : {df_B.index[0]} to {df_B.index[-1]}")
print(f"  NaN count: {df_B['Dublin_airport'].isna().sum()} months (COVID gap)")
df_B
Setup B: 195 observations
  Period   : 2010-01 to 2026-03
  NaN count: 26 months (COVID gap)
Out[14]:
Dublin_airport
2010-01 604.572
2010-02 628.476
2010-03 752.478
2010-04 610.714
2010-05 803.855
... ...
2025-11 1279.279
2025-12 1276.540
2026-01 1197.109
2026-02 1152.538
2026-03 1402.866

195 rows × 1 columns

In [15]:
exp2 = TSForecastingExperiment()
exp2.setup(
    data=df_B,
    target='Dublin_airport',
    session_id=42,
    fh=12,
    fold=3,
    fold_strategy='expanding',
    seasonal_period=12,
    coverage=0.90,
    numeric_imputation_target='drift',
    verbose=True
)
  Description Value
0 session_id 42
1 Target Dublin_airport
2 Approach Univariate
3 Exogenous Variables Not Present
4 Original data shape (195, 1)
5 Transformed data shape (195, 1)
6 Transformed train set shape (183, 1)
7 Transformed test set shape (12, 1)
8 Rows with missing values 13.3%
9 Fold Generator ExpandingWindowSplitter
10 Fold Number 3
11 Enforce Prediction Interval False
12 Splits used for hyperparameters all
13 User Defined Seasonal Period(s) 12
14 Ignore Seasonality Test False
15 Seasonality Detection Algo user_defined
16 Max Period to Consider 60
17 Seasonal Period(s) Tested [12]
18 Significant Seasonal Period(s) [12]
19 Significant Seasonal Period(s) without Harmonics [12]
20 Remove Harmonics False
21 Harmonics Order Method harmonic_max
22 Num Seasonalities to Use 1
23 All Seasonalities to Use [12]
24 Primary Seasonality 12
25 Seasonality Present True
26 Seasonality Type mul
27 Target Strictly Positive True
28 Target White Noise No
29 Recommended d 1
30 Recommended Seasonal D 0
31 Preprocess True
32 Numerical Imputation (Target) drift
33 Transformation (Target) None
34 Scaling (Target) None
35 Feature Engineering (Target) - Reduced Regression False
36 CPU Jobs -1
37 Use GPU False
38 Log Experiment False
39 Experiment Name ts-default-name
40 USI bc48
Out[15]:
<pycaret.time_series.forecasting.oop.TSForecastingExperiment at 0x14d1635e0>

3.1 Statistical Validation (Setup B)¶

In [16]:
exp2.check_stats()
Out[16]:
Test Test Name Data Property Setting Value
0 Summary Statistics Transformed Length 195.0
1 Summary Statistics Transformed # Missing Values 0.0
2 Summary Statistics Transformed Mean 1126.863943
3 Summary Statistics Transformed Median 1138.428
4 Summary Statistics Transformed Standard Deviation 300.028783
5 Summary Statistics Transformed Variance 90017.270496
6 Summary Statistics Transformed Kurtosis -0.394798
7 Summary Statistics Transformed Skewness 0.155182
8 Summary Statistics Transformed # Distinct Values 194.0
9 White Noise Ljung-Box Transformed Test Statictic {'alpha': 0.05, 'K': 24} 1286.307799
10 White Noise Ljung-Box Transformed Test Statictic {'alpha': 0.05, 'K': 48} 1720.25752
11 White Noise Ljung-Box Transformed p-value {'alpha': 0.05, 'K': 24} 0.0
12 White Noise Ljung-Box Transformed p-value {'alpha': 0.05, 'K': 48} 0.0
13 White Noise Ljung-Box Transformed White Noise {'alpha': 0.05, 'K': 24} 0.0
14 White Noise Ljung-Box Transformed White Noise {'alpha': 0.05, 'K': 48} 0.0
15 Stationarity ADF Transformed Stationarity {'alpha': 0.05} False
16 Stationarity ADF Transformed p-value {'alpha': 0.05} 0.945526
17 Stationarity ADF Transformed Test Statistic {'alpha': 0.05} -0.137938
18 Stationarity ADF Transformed Critical Value 1% {'alpha': 0.05} -3.467005
19 Stationarity ADF Transformed Critical Value 5% {'alpha': 0.05} -2.877644
20 Stationarity ADF Transformed Critical Value 10% {'alpha': 0.05} -2.575355
21 Stationarity KPSS Transformed Trend Stationarity {'alpha': 0.05} True
22 Stationarity KPSS Transformed p-value {'alpha': 0.05} 0.1
23 Stationarity KPSS Transformed Test Statistic {'alpha': 0.05} 0.10397
24 Stationarity KPSS Transformed Critical Value 10% {'alpha': 0.05} 0.119
25 Stationarity KPSS Transformed Critical Value 5% {'alpha': 0.05} 0.146
26 Stationarity KPSS Transformed Critical Value 2.5% {'alpha': 0.05} 0.176
27 Stationarity KPSS Transformed Critical Value 1% {'alpha': 0.05} 0.216
28 Normality Shapiro Transformed Normality {'alpha': 0.05} False
29 Normality Shapiro Transformed p-value {'alpha': 0.05} 0.049615

3.2 Inspect Drift Imputation Applied to COVID Gap¶

In [17]:
# Visualise how drift imputation filled the 26 missing months
df_transformed = exp2.get_config('dataset_transformed')

fig, ax = plt.subplots(figsize=(16, 5))
df_transformed['Dublin_airport'].plot(ax=ax, color='steelblue', lw=1.5, label='After imputation')
ax.axvspan(
    df_transformed.index.asof(pd.Period('2020-01', 'M')),
    df_transformed.index.asof(pd.Period('2022-02', 'M')),
    alpha=0.15, color='red', label='COVID gap (drift filled)'
)
ax.set_title('Dataset After Drift Imputation – COVID Gap Highlighted')
ax.set_ylabel('Passengers (thousands)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [18]:
exp2.plot_model(plot='cv')
exp2.plot_model(plot='train_test_split')
In [19]:
best_B   = exp2.compare_models(sort='SMAPE', n_select=3)
results_B = exp2.pull()  # called immediately after compare_models
  Model MASE RMSSE MAE RMSE MAPE SMAPE R2 TT (Sec)
exp_smooth Exponential Smoothing 0.8150 0.7053 68.3717 81.6027 0.0543 0.0548 0.8293 0.0300
et_cds_dt Extra Trees w/ Cond. Deseasonalize & Detrending 0.9256 0.7908 76.7661 90.8576 0.0601 0.0594 0.7638 0.1333
omp_cds_dt Orthogonal Matching Pursuit w/ Cond. Deseasonalize & Detrending 0.9179 0.7500 77.9127 87.8537 0.0627 0.0614 0.8346 0.0567
gbr_cds_dt Gradient Boosting w/ Cond. Deseasonalize & Detrending 1.0126 0.8466 83.8836 97.0198 0.0652 0.0645 0.7165 0.0767
rf_cds_dt Random Forest w/ Cond. Deseasonalize & Detrending 1.0271 0.8798 85.3169 101.0986 0.0674 0.0663 0.7094 0.1433
dt_cds_dt Decision Tree w/ Cond. Deseasonalize & Detrending 1.0579 0.9629 90.0574 112.4471 0.0641 0.0664 0.7111 0.0533
knn_cds_dt K Neighbors w/ Cond. Deseasonalize & Detrending 1.0946 0.9350 91.6093 107.6575 0.0716 0.0695 0.6763 0.1233
lightgbm_cds_dt Light Gradient Boosting w/ Cond. Deseasonalize & Detrending 1.1015 0.9255 90.8624 105.7087 0.0721 0.0701 0.6397 0.1133
ada_cds_dt AdaBoost w/ Cond. Deseasonalize & Detrending 1.1980 1.0012 99.0010 114.6962 0.0797 0.0774 0.6009 0.0667
arima ARIMA 1.3680 1.0628 109.8095 118.8804 0.0865 0.0897 0.2589 0.0300
snaive Seasonal Naive Forecaster 1.4778 1.2156 121.5364 138.8985 0.0931 0.0951 0.3747 0.0367
theta Theta Forecaster 1.3855 1.0569 111.6768 118.8350 0.0904 0.0976 0.3182 0.0333
huber_cds_dt Huber w/ Cond. Deseasonalize & Detrending 1.4851 1.2577 120.8226 142.1904 0.0933 0.0987 0.1714 0.0667
ets ETS 1.4810 1.1344 120.0975 128.1750 0.0966 0.1031 0.2951 0.0300
br_cds_dt Bayesian Ridge w/ Cond. Deseasonalize & Detrending 1.5906 1.2852 129.0605 145.0602 0.1016 0.1050 0.0976 0.0600
auto_arima Auto ARIMA 1.6152 1.2883 131.1861 145.4063 0.1043 0.1086 0.0849 2.3433
en_cds_dt Elastic Net w/ Cond. Deseasonalize & Detrending 1.7032 1.3734 137.8067 154.6299 0.1087 0.1134 -0.0901 0.0567
lasso_cds_dt Lasso w/ Cond. Deseasonalize & Detrending 1.7032 1.3734 137.8044 154.6274 0.1087 0.1134 -0.0901 0.0667
llar_cds_dt Lasso Least Angular Regressor w/ Cond. Deseasonalize & Detrending 1.7032 1.3734 137.8036 154.6269 0.1087 0.1134 -0.0901 0.0600
lr_cds_dt Linear w/ Cond. Deseasonalize & Detrending 1.7039 1.3740 137.8607 154.6899 0.1088 0.1135 -0.0913 0.0567
ridge_cds_dt Ridge w/ Cond. Deseasonalize & Detrending 1.7039 1.3740 137.8603 154.6895 0.1088 0.1135 -0.0913 0.0733
stlf STLF 1.8791 1.5711 155.9059 180.9973 0.1162 0.1215 0.0298 0.0433
polytrend Polynomial Trend Forecaster 2.5179 2.0991 213.2533 244.8161 0.1715 0.1603 -0.3413 0.0167
croston Croston 2.5137 2.1289 214.7543 250.5319 0.1532 0.1623 -0.3177 0.0200
grand_means Grand Means Forecaster 3.3003 2.9833 282.2588 351.1370 0.1895 0.2206 -1.5736 0.0167
naive Naive Forecaster 3.3298 2.7725 278.2114 320.5331 0.2005 0.2259 -1.6132 0.0200

3.3 Create and Tune Models (Setup B)¶

In [20]:
models_B = ['exp_smooth', 'arima', 'et_cds_dt', 'omp_cds_dt', 'gbr_cds_dt']
tune_B   = ['exp_smooth', 'arima', 'et_cds_dt', 'omp_cds_dt', 'gbr_cds_dt']

resultados_B   = {}
resultados_t_B = {}

for model_name in models_B:
    print(f'Creating: {model_name}')
    resultados_B[model_name] = exp2.create_model(model_name)

for model_name in tune_B:
    print(f'Tuning: {model_name}')
    resultados_t_B[model_name] = exp2.tune_model(resultados_B[model_name])
Creating: exp_smooth
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.0243 0.9165 79.7430 99.7027 0.0648 0.0683 0.6718
1 2023-03 0.6694 0.5116 57.4432 60.8166 0.0426 0.0430 0.9269
2 2024-03 0.7512 0.6876 67.9290 84.2886 0.0556 0.0530 0.8894
Mean NaT 0.8150 0.7053 68.3717 81.6027 0.0543 0.0548 0.8293
SD NaT 0.1517 0.1658 9.1092 15.9884 0.0091 0.0104 0.1125
Creating: arima
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 3.0688 2.3258 238.9059 253.0125 0.1953 0.2043 -1.1137
1 2023-03 0.6672 0.5628 57.2550 66.8977 0.0394 0.0405 0.9115
2 2024-03 0.3679 0.2997 33.2678 36.7311 0.0247 0.0244 0.9790
Mean NaT 1.3680 1.0628 109.8095 118.8804 0.0865 0.0897 0.2589
SD NaT 1.2088 0.8996 91.8087 95.6419 0.0772 0.0813 0.9710
Creating: et_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.2907 1.1048 100.4847 120.1832 0.0856 0.0826 0.5231
1 2023-03 0.9880 0.8068 84.7826 95.9076 0.0586 0.0603 0.8181
2 2024-03 0.4980 0.4608 45.0310 56.4819 0.0362 0.0352 0.9503
Mean NaT 0.9256 0.7908 76.7661 90.8576 0.0601 0.0594 0.7638
SD NaT 0.3266 0.2632 23.3378 26.2500 0.0202 0.0194 0.1786
Creating: omp_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.8654 0.6910 67.3718 75.1735 0.0593 0.0583 0.8134
1 2023-03 0.9486 0.7317 81.4045 86.9760 0.0624 0.0617 0.8504
2 2024-03 0.9396 0.8273 84.9619 101.4115 0.0665 0.0642 0.8399
Mean NaT 0.9179 0.7500 77.9127 87.8537 0.0627 0.0614 0.8346
SD NaT 0.0373 0.0571 7.5938 10.7296 0.0029 0.0024 0.0156
Creating: gbr_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.4489 1.2430 112.7988 135.2171 0.0958 0.0930 0.3963
1 2023-03 1.0486 0.8428 89.9845 100.1785 0.0626 0.0644 0.8015
2 2024-03 0.5404 0.4541 48.8675 55.6638 0.0370 0.0363 0.9518
Mean NaT 1.0126 0.8466 83.8836 97.0198 0.0652 0.0645 0.7165
SD NaT 0.3717 0.3221 26.4540 32.5542 0.0241 0.0231 0.2346
Tuning: exp_smooth
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.7210 0.6546 56.1295 71.2073 0.0458 0.0471 0.8326
1 2023-03 0.5332 0.4535 45.7605 53.9062 0.0345 0.0349 0.9425
2 2024-03 0.7777 0.6945 70.3248 85.1325 0.0560 0.0536 0.8872
Mean NaT 0.6773 0.6009 57.4049 70.0820 0.0454 0.0452 0.8874
SD NaT 0.1045 0.1055 10.0688 12.7729 0.0088 0.0078 0.0449
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    0.6s finished
Tuning: arima
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 2.2248 1.8587 173.2023 202.1963 0.1507 0.1451 -0.3499
1 2023-03 1.3013 1.1206 111.6712 133.2062 0.0757 0.0798 0.6491
2 2024-03 0.3656 0.3009 33.0603 36.8801 0.0240 0.0239 0.9788
Mean NaT 1.2972 1.0934 105.9779 124.0942 0.0834 0.0829 0.4260
SD NaT 0.7590 0.6363 57.3542 67.7969 0.0520 0.0495 0.5649
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    0.8s finished
Tuning: et_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.4945 1.3302 116.3518 144.7052 0.1048 0.0969 0.3086
1 2023-03 0.7065 0.5791 60.6265 68.8395 0.0436 0.0446 0.9063
2 2024-03 0.4087 0.3400 36.9510 41.6715 0.0268 0.0268 0.9730
Mean NaT 0.8699 0.7498 71.3098 85.0721 0.0584 0.0561 0.7293
SD NaT 0.4581 0.4219 33.2838 43.6013 0.0335 0.0298 0.2987
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    1.9s finished
Tuning: omp_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 1.0662 0.8210 83.0038 89.3081 0.0712 0.0686 0.7366
1 2023-03 0.5204 0.4508 44.6565 53.5896 0.0364 0.0361 0.9432
2 2024-03 0.5792 0.4769 52.3710 58.4544 0.0410 0.0399 0.9468
Mean NaT 0.7219 0.5829 60.0104 67.1174 0.0495 0.0482 0.8756
SD NaT 0.2446 0.1687 16.5610 15.8164 0.0155 0.0145 0.0982
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    0.9s finished
Tuning: gbr_cds_dt
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.5874 0.5166 45.7298 56.1968 0.0397 0.0388 0.8957
1 2023-03 1.5374 1.1867 131.9332 141.0611 0.0950 0.1003 0.6065
2 2024-03 0.7360 0.6098 66.5468 74.7480 0.0505 0.0497 0.9130
Mean NaT 0.9536 0.7710 81.4033 90.6686 0.0617 0.0630 0.8051
SD NaT 0.4172 0.2964 36.7268 36.4288 0.0239 0.0268 0.1406
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 10 concurrent workers.
[Parallel(n_jobs=-1)]: Done  30 out of  30 | elapsed:    1.5s finished

3.4 Blend and Stack (Setup B)¶

In [21]:
# Blend – weighted average of top 3 models
blended_B = exp2.blend_models(best_B)
print("Blended model created – Setup B")
  cutoff MASE RMSSE MAE RMSE MAPE SMAPE R2
0 2022-03 0.8326 0.6504 64.8159 70.7484 0.0536 0.0536 0.8347
1 2023-03 0.8681 0.6515 74.4963 77.4452 0.0545 0.0550 0.8814
2 2024-03 0.6663 0.6255 60.2456 76.6786 0.0492 0.0473 0.9085
Mean NaT 0.7890 0.6425 66.5193 74.9574 0.0524 0.0520 0.8749
SD NaT 0.0880 0.0120 5.9412 2.9926 0.0023 0.0034 0.0304
Blended model created – Setup B

3.5 Decomposition, ACF and Diagnostics (Setup B)¶

In [22]:
# Seasonal decomposition and ACF – best tuned model
exp2.plot_model(resultados_t_B['exp_smooth'], plot='decomp')
exp2.plot_model(resultados_t_B['exp_smooth'], plot='acf')
In [23]:
for model_name in tune_B:
    print(f'Diagnostics: {model_name}')
    exp2.plot_model(estimator=resultados_t_B[model_name], plot='diagnostics')
Diagnostics: exp_smooth
Diagnostics: arima
Diagnostics: et_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
Diagnostics: omp_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
Diagnostics: gbr_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In [24]:
for model_name in tune_B:
    print(f'In-sample: {model_name}')
    exp2.plot_model(resultados_t_B[model_name], plot='insample')
In-sample: exp_smooth
In-sample: arima
In-sample: et_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In-sample: omp_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.
In-sample: gbr_cds_dt
In sample predictions has not been implemented for this estimator of type 'BaseCdsDtForecaster' in `sktime`. When this is implemented, it will be enabled by default in pycaret.

3.6 Finalize and Store Forecasts (Setup B)¶

In [25]:
forecasts_B        = {}
resultados_final_B = {}

# Include blend and stack alongside individual models
all_models_B = {**resultados_B, 'blend': blended_B}

for model_name, model_obj in all_models_B.items():
    source      = resultados_t_B.get(model_name, model_obj)
    final_model = exp2.finalize_model(source)
    pred        = exp2.predict_model(final_model, fh=12)
    forecasts_B[model_name]        = pred['y_pred']
    resultados_final_B[model_name] = final_model
    print(f'Forecast – {model_name}:')
    display(pred)
    # Forecast with 90% confidence interval
    exp2.plot_model(final_model, plot='forecast', data_kwargs={'fh': 12})
    print()
Forecast – exp_smooth:
y_pred
2026-04 1560.9654
2026-05 1677.9206
2026-06 1772.7468
2026-07 1952.0170
2026-08 1965.9407
2026-09 1720.8029
2026-10 1637.8686
2026-11 1346.1215
2026-12 1316.0814
2027-01 1256.8653
2027-02 1248.8532
2027-03 1402.8660
Forecast – arima:
y_pred
2026-04 1566.4898
2026-05 1704.6223
2026-06 1767.7433
2026-07 1923.9976
2026-08 1971.5737
2026-09 1708.8255
2026-10 1640.9376
2026-11 1326.3116
2026-12 1323.4354
2027-01 1244.0834
2027-02 1200.0322
2027-03 1450.0576
Forecast – et_cds_dt:
y_pred
2026-04 1538.1621
2026-05 1648.7140
2026-06 1751.9956
2026-07 1913.4137
2026-08 1933.8756
2026-09 1693.3180
2026-10 1605.3448
2026-11 1326.0590
2026-12 1295.6060
2027-01 1199.4461
2027-02 1198.7948
2027-03 1437.5339
Forecast – omp_cds_dt:
y_pred
2026-04 1580.8557
2026-05 1673.4297
2026-06 1758.9376
2026-07 1917.8768
2026-08 1928.3096
2026-09 1688.5507
2026-10 1600.3482
2026-11 1324.8890
2026-12 1295.2040
2027-01 1205.1776
2027-02 1202.0565
2027-03 1431.2530
Forecast – gbr_cds_dt:
y_pred
2026-04 1596.0229
2026-05 1729.8815
2026-06 1850.1600
2026-07 2057.7315
2026-08 2085.9122
2026-09 1851.0805
2026-10 1768.1470
2026-11 1479.5906
2026-12 1464.3070
2027-01 1373.3573
2027-02 1382.6150
2027-03 1670.9396
Forecast – blend:
y_pred
2026-04 1557.8026
2026-05 1664.1409
2026-06 1748.8814
2026-07 1906.3237
2026-08 1925.7698
2026-09 1711.8875
2026-10 1618.0851
2026-11 1377.9164
2026-12 1351.4142
2027-01 1303.8262
2027-02 1301.5762
2027-03 1437.5363


4. Forecast Comparison – All Models¶

Forecasts collected from predict_model during the finalize loop — no dependency on pull().

In [26]:
# Build comparison tables from stored dictionaries
df_forecast_A = pd.DataFrame(forecasts_A)
df_forecast_A.index = df_forecast_A.index.astype(str)
df_forecast_A.columns = [f'{c}_A' for c in df_forecast_A.columns]

df_forecast_B = pd.DataFrame(forecasts_B)
df_forecast_B.index = df_forecast_B.index.astype(str)
df_forecast_B.columns = [f'{c}_B' for c in df_forecast_B.columns]

df_comparison = pd.concat([df_forecast_A, df_forecast_B], axis=1)
df_comparison.index.name = 'Period'

print('12-Month Forecast – Setup A (with COVID) vs Setup B (without COVID):')
print(df_comparison.round(0).astype(int).to_string())
12-Month Forecast – Setup A (with COVID) vs Setup B (without COVID):
         exp_smooth_A  arima_A  ets_A  lasso_cds_dt_A  lr_cds_dt_A  blend_A  exp_smooth_B  arima_B  et_cds_dt_B  omp_cds_dt_B  gbr_cds_dt_B  blend_B
Period                                                                                                                                              
2026-04          1582     1550   1574            1542         1558     1587          1561     1566         1538          1581          1596     1558
2026-05          1721     1657   1711            1629         1641     1705          1678     1705         1649          1673          1730     1664
2026-06          1833     1736   1823            1705         1709     1784          1773     1768         1752          1759          1850     1749
2026-07          2042     1882   2032            1932         1840     1948          1952     1924         1913          1918          2058     1906
2026-08          2070     1926   2061            2057         1832     1968          1966     1972         1934          1928          2086     1926
2026-09          1794     1755   1785            1793         1625     1718          1721     1709         1693          1689          1851     1712
2026-10          1706     1701   1697            1681         1531     1634          1638     1641         1605          1600          1768     1618
2026-11          1370     1489   1360            1320         1289     1334          1346     1326         1326          1325          1480     1378
2026-12          1340     1467   1327            1285         1243     1306          1316     1323         1296          1295          1464     1351
2027-01          1255     1410   1239            1149         1158     1221          1257     1244         1199          1205          1373     1304
2027-02          1254     1412   1237            1124         1161     1205          1249     1200         1199          1202          1383     1302
2027-03          1465     1546   1444            1269         1290     1399          1403     1450         1438          1431          1671     1438
In [27]:
# Line chart – all Setup B models including blend and stack
fig, ax = plt.subplots(figsize=(14, 6))

colors_map = {
    'exp_smooth' : '#2ecc71',
    'arima'      : '#3498db',
    'ets'        : '#9b59b6',
    'knn_cds_dt' : '#e67e22',
    'llar_cds_dt': '#1abc9c',
    'lasso_cds_dt':'#e74c3c',
    'blend'      : '#2c3e50',
    'stack'      : '#8e44ad'
}

for model_name, series in forecasts_B.items():
    lw   = 3 if model_name in ['blend', 'stack'] else 1.5
    ls   = '-' if model_name in ['blend', 'stack'] else '--'
    color = colors_map.get(model_name, 'grey')
    ax.plot(series.index.astype(str), series.values,
            marker='o', label=f'{model_name} (no COVID)',
            color=color, lw=lw, ls=ls, ms=5)

# Overlay exp_smooth from Setup A
if 'exp_smooth' in forecasts_A:
    s = forecasts_A['exp_smooth']
    ax.plot(s.index.astype(str), s.values,
            'r--s', label='exp_smooth (with COVID)', lw=2, ms=5)

ax.set_xlabel('Month')
ax.set_ylabel('Passengers (thousands)')
ax.set_title('PyCaret – 12-Month Forecast: All Models + Ensemble')
ax.legend(loc='upper right', fontsize=8, ncol=2)
ax.grid(True, alpha=0.3)
plt.xticks(rotation=30, ha='right')
plt.tight_layout()
plt.show()
No description has been provided for this image
In [28]:
# Bar chart – peak month forecast by model
july_str = [i for i in list(forecasts_B.values())[0].index.astype(str) if '-07' in i]

if july_str:
    july_period = july_str[0]
    july_vals   = {m: forecasts_B[m][forecasts_B[m].index.astype(str) == july_period].values[0]
                   for m in forecasts_B}

    fig, ax = plt.subplots(figsize=(12, 5))
    colors  = [colors_map.get(m, 'grey') for m in july_vals]
    bars    = ax.bar(july_vals.keys(), july_vals.values(), color=colors, alpha=0.85)

    for b in bars:
        ax.text(b.get_x()+b.get_width()/2, b.get_height()+5,
                f'{b.get_height():,.0f}k', ha='center', fontsize=9)

    ax.set_ylabel('Passengers (thousands)')
    ax.set_title(f'PyCaret Setup B – Peak Month ({july_period}) Forecast by Model')
    ax.grid(True, alpha=0.3, axis='y')
    plt.xticks(rotation=20, ha='right')
    plt.tight_layout()
    plt.show()
No description has been provided for this image
In [29]:
# Blend vs Stack vs best individual – direct comparison
fig, ax = plt.subplots(figsize=(12, 5))

highlight = ['exp_smooth', 'arima', 'blend', 'stack']
styles    = ['g--', 'b--', 'k-', 'm-']
lws       = [1.5, 1.5, 2.5, 2.5]

for model_name, style, lw in zip(highlight, styles, lws):
    if model_name in forecasts_B:
        s = forecasts_B[model_name]
        ax.plot(s.index.astype(str), s.values,
                style, label=model_name, lw=lw, marker='o', ms=5)

ax.set_xlabel('Month')
ax.set_ylabel('Passengers (thousands)')
ax.set_title('PyCaret Setup B – Individual vs Ensemble Forecasts')
ax.legend()
ax.grid(True, alpha=0.3)
plt.xticks(rotation=30, ha='right')
plt.tight_layout()
plt.show()
No description has been provided for this image
In [30]:
# exp_smooth: with COVID vs without COVID
if 'exp_smooth' in forecasts_A and 'exp_smooth' in forecasts_B:
    s_A = forecasts_A['exp_smooth']
    s_B = forecasts_B['exp_smooth']

    fig, ax = plt.subplots(figsize=(12, 5))
    ax.plot(s_A.index.astype(str), s_A.values,
            'r--o', label='exp_smooth – With COVID', lw=2, ms=6)
    ax.plot(s_B.index.astype(str), s_B.values,
            'g-o',  label='exp_smooth – No COVID',   lw=2, ms=6)
    ax.set_xlabel('Month')
    ax.set_ylabel('Passengers (thousands)')
    ax.set_title('exp_smooth: Impact of COVID Period on Forecast')
    ax.legend()
    ax.grid(True, alpha=0.3)
    plt.xticks(range(len(s_A)), s_A.index.astype(str), rotation=30, ha='right')
    plt.tight_layout()
    plt.show()
No description has been provided for this image

4.4 Forecast vs Actual CSO Data¶

In [31]:
# Dados reais (Abr 2025 – Mar 2026) + Forecast ETS Setup A (Abr 2026 – Mar 2027)
# ETS foi o melhor modelo no Setup A (com COVID)
data_A = {
    '2025-04': 1519.066, '2025-05': 1657.095, '2025-06': 1720.507,
    '2025-07': 1877.294, '2025-08': 1924.106, '2025-09': 1661.162,
    '2025-10': 1593.932, '2025-11': 1279.279, '2025-12': 1276.540,
    '2026-01': 1197.109, '2026-02': 1152.538, '2026-03': 1402.866,
    '2026-04': 1573.684, '2026-05': 1711.488, '2026-06': 1822.999,
    '2026-07': 2032.111, '2026-08': 2060.745, '2026-09': 1784.922,
    '2026-10': 1696.599, '2026-11': 1360.367, '2026-12': 1326.905,
    '2027-01': 1239.111, '2027-02': 1237.357, '2027-03': 1443.539
}

df_ets_A = pd.DataFrame.from_dict(data_A, orient='index', columns=['Dublin_airport'])
df_ets_A.index.name = 'Date'
df_ets_A['MoM_%'] = df_ets_A['Dublin_airport'].pct_change() * 100
df_ets_A['YoY_%'] = df_ets_A['Dublin_airport'].pct_change(periods=12) * 100
df_ets_A = df_ets_A.round(2)
df_ets_A['Source'] = ['Actual'] * 12 + ['ETS Forecast (Setup A)'] * 12
print('ETS Setup A – Actual + Forecast:')
print(df_ets_A.to_string())
ETS Setup A – Actual + Forecast:
         Dublin_airport  MoM_%  YoY_%                  Source
Date                                                         
2025-04         1519.07    NaN    NaN                  Actual
2025-05         1657.10   9.09    NaN                  Actual
2025-06         1720.51   3.83    NaN                  Actual
2025-07         1877.29   9.11    NaN                  Actual
2025-08         1924.11   2.49    NaN                  Actual
2025-09         1661.16 -13.67    NaN                  Actual
2025-10         1593.93  -4.05    NaN                  Actual
2025-11         1279.28 -19.74    NaN                  Actual
2025-12         1276.54  -0.21    NaN                  Actual
2026-01         1197.11  -6.22    NaN                  Actual
2026-02         1152.54  -3.72    NaN                  Actual
2026-03         1402.87  21.72    NaN                  Actual
2026-04         1573.68  12.18   3.60  ETS Forecast (Setup A)
2026-05         1711.49   8.76   3.28  ETS Forecast (Setup A)
2026-06         1823.00   6.52   5.96  ETS Forecast (Setup A)
2026-07         2032.11  11.47   8.25  ETS Forecast (Setup A)
2026-08         2060.74   1.41   7.10  ETS Forecast (Setup A)
2026-09         1784.92 -13.38   7.45  ETS Forecast (Setup A)
2026-10         1696.60  -4.95   6.44  ETS Forecast (Setup A)
2026-11         1360.37 -19.82   6.34  ETS Forecast (Setup A)
2026-12         1326.90  -2.46   3.95  ETS Forecast (Setup A)
2027-01         1239.11  -6.62   3.51  ETS Forecast (Setup A)
2027-02         1237.36  -0.14   7.36  ETS Forecast (Setup A)
2027-03         1443.54  16.66   2.90  ETS Forecast (Setup A)
In [32]:
# Dados reais (Abr 2025 – Mar 2026) + Forecast exp_smooth Setup B (Abr 2026 – Mar 2027)
# Exponential Smoothing foi o melhor modelo no Setup B (sem COVID)
data_B = {
    '2025-04': 1519.066, '2025-05': 1657.095, '2025-06': 1720.507,
    '2025-07': 1877.294, '2025-08': 1924.106, '2025-09': 1661.162,
    '2025-10': 1593.932, '2025-11': 1279.279, '2025-12': 1276.540,
    '2026-01': 1197.109, '2026-02': 1152.538, '2026-03': 1402.866,
    '2026-04': 1560.965, '2026-05': 1677.921, '2026-06': 1772.747,
    '2026-07': 1952.017, '2026-08': 1965.941, '2026-09': 1720.803,
    '2026-10': 1637.869, '2026-11': 1346.122, '2026-12': 1316.081,
    '2027-01': 1256.865, '2027-02': 1248.853, '2027-03': 1402.866
}

df_exp_B = pd.DataFrame.from_dict(data_B, orient='index', columns=['Dublin_airport'])
df_exp_B.index.name = 'Date'
df_exp_B['MoM_%'] = df_exp_B['Dublin_airport'].pct_change() * 100
df_exp_B['YoY_%'] = df_exp_B['Dublin_airport'].pct_change(periods=12) * 100
df_exp_B = df_exp_B.round(2)
df_exp_B['Source'] = ['Actual'] * 12 + ['exp_smooth Forecast (Setup B)'] * 12
print('exp_smooth Setup B – Actual + Forecast:')
print(df_exp_B.to_string())
exp_smooth Setup B – Actual + Forecast:
         Dublin_airport  MoM_%  YoY_%                         Source
Date                                                                
2025-04         1519.07    NaN    NaN                         Actual
2025-05         1657.10   9.09    NaN                         Actual
2025-06         1720.51   3.83    NaN                         Actual
2025-07         1877.29   9.11    NaN                         Actual
2025-08         1924.11   2.49    NaN                         Actual
2025-09         1661.16 -13.67    NaN                         Actual
2025-10         1593.93  -4.05    NaN                         Actual
2025-11         1279.28 -19.74    NaN                         Actual
2025-12         1276.54  -0.21    NaN                         Actual
2026-01         1197.11  -6.22    NaN                         Actual
2026-02         1152.54  -3.72    NaN                         Actual
2026-03         1402.87  21.72    NaN                         Actual
2026-04         1560.96  11.27   2.76  exp_smooth Forecast (Setup B)
2026-05         1677.92   7.49   1.26  exp_smooth Forecast (Setup B)
2026-06         1772.75   5.65   3.04  exp_smooth Forecast (Setup B)
2026-07         1952.02  10.11   3.98  exp_smooth Forecast (Setup B)
2026-08         1965.94   0.71   2.17  exp_smooth Forecast (Setup B)
2026-09         1720.80 -12.47   3.59  exp_smooth Forecast (Setup B)
2026-10         1637.87  -4.82   2.76  exp_smooth Forecast (Setup B)
2026-11         1346.12 -17.81   5.23  exp_smooth Forecast (Setup B)
2026-12         1316.08  -2.23   3.10  exp_smooth Forecast (Setup B)
2027-01         1256.86  -4.50   4.99  exp_smooth Forecast (Setup B)
2027-02         1248.85  -0.64   8.36  exp_smooth Forecast (Setup B)
2027-03         1402.87  12.33   0.00  exp_smooth Forecast (Setup B)

5. References¶

Ali, M. (2020) PyCaret: An open source, low-code machine learning library in Python. Available at: https://pycaret.org (Accessed: March 2026).

Central Statistics Office (2025) Air and Sea Travel Statistics – ASM01. Available at: https://data.cso.ie/table/ASM01 (Accessed: January 2026).

Moez Ali (2020) PyCaret: An open source, low-code machine learning library in Python. Available at: https://pycaret.org (Accessed: March 2026).

PJalgotrader (2024) PyCaret Time Series Notebook – Deep Forecasting USU. Available at: https://github.com/PJalgotrader/Deep_forecasting-USU/blob/main/Platforms%20and%20tools/PyCaret/PyCaret-timeseries.ipynb (Accessed: March 2026).

PyCaret Documentation (2024) Other Setup Parameters – Preprocessing. Available at: https://pycaret.gitbook.io/docs/get-started/preprocessing/other-setup-parameters (Accessed: March 2026).

PyCaret Documentation (2024) Datasets API Reference. Available at: https://pycaret.readthedocs.io/en/latest/api/datasets.html#pycaret.datasets.get_data (Accessed: March 2026).

PyCaret Documentation (2024) Time Series Forecasting API. Available at: https://pycaret.readthedocs.io/en/stable/api/time_series.html (Accessed: March 2026).

Sktime developers (2024) sktime: A unified framework for machine learning with time series. Available at: https://www.sktime.net (Accessed: March 2026).

Hyndman, R.J. and Athanasopoulos, G. (2021) Forecasting: Principles and Practice, 3rd ed. OTexts. Available at: https://otexts.com/fpp3/ (Accessed: March 2026).

YouTube (2024) PyCaret Time Series Forecasting Tutorial. Available at: https://www.youtube.com/watch?v=V7K-pFxHop4 (Accessed: March 2026).

YouTube (2024) Time Series Forecasting with PyCaret. Available at: https://www.youtube.com/watch?v=uypdqSPdN60 (Accessed: March 2026).