DATA624 - Homework 5

Author

Anthony Josue Roman

Exercise 8.1

Part A

import pandas as pd
from statsmodels.tsa.holtwinters import SimpleExpSmoothing

df = pd.read_csv("aus_livestock.csv")

pigs = df[df["unique_id"] == "Victoria_Pigs"].copy()

pigs["ds"] = pd.to_datetime(pigs["ds"])
pigs = pigs.sort_values("ds")

model = SimpleExpSmoothing(pigs["y"])

fit = model.fit(optimized=True)

alpha = fit.model.params["smoothing_level"]
level0 = fit.model.params["initial_level"]

forecast = fit.forecast(4)

print("alpha:", alpha)
print("initial level:", level0)
print("4-step forecasts:")
print(forecast)
alpha: 0.323915461706749
initial level: 94200.0
4-step forecasts:
558    95183.971069
559    95183.971069
560    95183.971069
561    95183.971069
dtype: float64

Simple Exponential Smoothing was used to analyze the Victoria pigs series. The smoothing parameter was calculated to be \(\alpha = 0.3239\) with an initial level of 94,200.

All the forecasts for the four-step-ahead values are equal to 95,183.97. This is because the Simple Exponential Smoothing model has no trend or seasonality component and therefore reverts to the last calculated level of the series.

Hence, the next four period forecasts remain the same at around 95,184.

Part B

residuals = fit.resid

s = residuals.std(ddof=1)

first_forecast = forecast.iloc[0]

lower = first_forecast - 1.96 * s
upper = first_forecast + 1.96 * s

print("Residual SD:", s)
print("First forecast:", first_forecast)
print("95% prediction interval:", lower, upper)
Residual SD: 9351.811357151031
First forecast: 95183.97106900971
95% prediction interval: 76854.42080899369 113513.52132902574

Using the fitted simple exponential smoothing model, the initial forecast is 95,183.97, with a residual standard deviation of 9,351.81.

With such a level of uncertainty involved in the forecast, the 95% prediction interval for the next value is approximately between 76,854.42 and 113,513.52. This means that there is a 95% chance that the next value will fall within that range.

The width of the interval reflects the variability present in the data and indicates a level of uncertainty with the forecast.

Exercise 8.5

Part A

import pandas as pd
import matplotlib.pyplot as plt

global_economy = pd.read_csv("global_economy.csv")

global_economy["ds"] = pd.to_datetime(global_economy["ds"], format="%Y")

country = "United States"

exports = global_economy.loc[
    global_economy["unique_id"] == country,
    ["unique_id", "ds", "Exports"]
].dropna().sort_values("ds")

plt.figure(figsize=(10, 5))
plt.plot(exports["ds"], exports["Exports"], linewidth=2)
plt.title(f"Exports: {country}")
plt.xlabel("Year")
plt.ylabel("Exports (% of GDP)")
plt.tight_layout()
plt.show()

The export data from the US tells a clear story. If we look at the data on an annual basis, there is no real seasonal variability to speak of. Looking at the data over time, we can see that the overall growth in exports continues to increase over the years, suggesting a long-run increase in exports as a percentage of GDP. Of course, there are some ups and downs along the way, including in the early 1980s and again in the early 2000s, likely reflecting an overall economic slowdown.

Part B

from statsforecast import StatsForecast
from statsforecast.models import AutoETS
import matplotlib.pyplot as plt

exports_df = exports.rename(columns={"Exports": "y"}).copy()
exports_df["unique_id"] = "US_exports"

sf = StatsForecast(
    models=[AutoETS(model="ANN", alias="ETS_ANN")],
    freq="YE"
)

fc_ann = sf.forecast(df=exports_df, h=10, fitted=True)

plt.figure(figsize=(10, 5))
plt.plot(exports_df["ds"], exports_df["y"], linewidth=2, label="Observed")
plt.plot(fc_ann["ds"], fc_ann["ETS_ANN"], linewidth=2, label="Forecast")
plt.title("ETS(A,N,N) Forecast for US Exports")
plt.xlabel("Year")
plt.ylabel("Exports (% of GDP)")
plt.legend()
plt.tight_layout()
plt.show()

The ETS(A,N,N) model is essentially a simple exponential smoothing model. It assumes a series has a level component but no trend or seasonal component. As a consequence, the forecast is constant and equal to the level component. In this case, the forecast is close to the last export value because it simply updates its level component using recent values.

Part C

from utilsforecast.evaluation import evaluate
from utilsforecast.losses import rmse

fitted_ann = sf.forecast_fitted_values()

rmse_ann = evaluate(fitted_ann, metrics=[rmse])

rmse_ann
unique_id metric ETS_ANN
0 US_exports rmse 0.626488

The value of RMSE for the ETS(A,N,N) model is 0.6265, indicating the magnitude of the errors in the data set. As it is in the same units as the response variable, it indicates that the average difference between observed values and the model’s fitted values is around 0.63 percent GDP.

Part D

from statsforecast import StatsForecast
from statsforecast.models import AutoETS
from utilsforecast.evaluation import evaluate
from utilsforecast.losses import rmse

sf_aan = StatsForecast(
    models=[AutoETS(model="AAN", alias="ETS_AAN")],
    freq="YE"
)

fc_aan = sf_aan.forecast(df=exports_df, h=10, fitted=True)

fitted_aan = sf_aan.forecast_fitted_values()
rmse_aan = evaluate(fitted_aan, metrics=[rmse])

rmse_aan
unique_id metric ETS_AAN
0 US_exports rmse 0.614423
print("ETS(A,N,N) RMSE =", rmse_ann["ETS_ANN"].iloc[0])
print("ETS(A,A,N) RMSE =", rmse_aan["ETS_AAN"].iloc[0])
ETS(A,N,N) RMSE = 0.6264880604471516
ETS(A,A,N) RMSE = 0.6144233054629571

The addition of the additive trend component to the level component of the ETS(A,N,N) model allows the ETS(A,A,N) model to account for any increases or decreases in the time series over time.

From the values of the RMSE, the ETS(A,N,N) model has an RMSE of 0.6265, whereas the ETS(A,A,N) model has an RMSE of 0.6144. This implies that the ETS(A,A,N) model provides a slightly better fit to the historical data on exports because of the inclusion of the trend component.

Part E

import matplotlib.pyplot as plt

plt.figure(figsize=(10,5))

plt.plot(exports_df["ds"], exports_df["y"], label="Observed", linewidth=2)
plt.plot(fc_ann["ds"], fc_ann["ETS_ANN"], label="ETS(A,N,N)", linewidth=2)
plt.plot(fc_aan["ds"], fc_aan["ETS_AAN"], label="ETS(A,A,N)", linewidth=2)

plt.title("Forecast Comparison for US Exports")
plt.xlabel("Year")
plt.ylabel("Exports (% of GDP)")
plt.legend()

plt.tight_layout()
plt.show()

However, the models differ with respect to how they account for the underlying series’ structure. In the case of ETS(A,N,N), the model only accounts for the level present in the series, meaning that the forecasted values will remain constant at the last estimated level. In the case of ETS(A,A,N), an additive trend is incorporated into the model, meaning that the forecasted values will continue with the trend that has been present in the series over time.

Part F

import numpy as np

res_ann = fitted_ann["y"] - fitted_ann["ETS_ANN"]
s_ann = res_ann.std(ddof=1)

first_forecast = fc_ann["ETS_ANN"].iloc[0]

lower = first_forecast - 1.96*s_ann
upper = first_forecast + 1.96*s_ann

lower, upper
(np.float64(10.675369345115506), np.float64(13.105997202061456))

A 95% prediction interval for the first forecast may be approximated by

\[ \hat{y} \pm 1.96s \]

where \(s\) represents the standard deviation of residuals in the model.

Using this formula to compute a prediction interval for the first forecast using the ETS(A,N,N) model, we obtain

\[ (10.68,\; 13.11) \]

This interval represents where the next observed export value is likely to fall with a 95% probability, assuming normal distribution of forecast errors.

Exercise 8.6

Plot of the Chinese GDP series

import pandas as pd
import matplotlib.pyplot as plt

china = pd.read_csv("chinese_gdp.csv")

china = china.rename(columns={
    "Country": "unique_id",
    "Date": "ds",
    "GDP": "y"
})

china["ds"] = pd.to_datetime(china["ds"])

china = china.sort_values("ds")

plt.figure(figsize=(10,5))
plt.plot(china["ds"], china["y"], linewidth=2)

plt.title("Chinese GDP")
plt.xlabel("Year")
plt.ylabel("GDP")

plt.tight_layout()
plt.show()

From the Chinese GDP figures, it is clear there is a constant and strong growth in GDP over the years, reflecting the high growth rate of China’s economy in recent decades. There is a pick-up in the rate of growth after 2000, indicating a high growth rate in China’s economy. As the figures are annual, there is no seasonal effect in this data series. Overall, it indicates a growth in GDP over the years, with the rate of increase in GDP increasing in recent years.

ETS Forecast

from statsforecast import StatsForecast
from statsforecast.models import AutoETS
import matplotlib.pyplot as plt

sf = StatsForecast(
    models=[AutoETS(model="ZZZ", alias="ETS")],
    freq="YE"
)

fc_china = sf.forecast(df=china, h=15)

plt.figure(figsize=(10,5))

plt.plot(china["ds"], china["y"], label="Observed", linewidth=2)
plt.plot(fc_china["ds"], fc_china["ETS"], label="Forecast", linewidth=2)

plt.title("Chinese GDP Forecast (ETS)")
plt.xlabel("Year")
plt.ylabel("GDP")

plt.legend()
plt.tight_layout()
plt.show()

As can be seen from the graph produced by the ETS model, the forecasts continue to move along the same high trend that we have been seeing with the Chinese GDP data. As we can see, the data has been steadily increasing over time, which is why the model has factored that in with a trend that allows the forecasts to increase over time as well.

However, as can be seen from the graph produced by the model, the forecasts increase very sharply, which is a problem with trend-based models when we see a high trend over time with our data. This can make the forecasts unrealistically high if we assume that the trend will continue indefinitely.

Forecast Comparison

sf_damped = StatsForecast(
    models=[AutoETS(model="AAN", damped=True, alias="Damped_ETS")],
    freq="YE"
)

fc_damped = sf_damped.forecast(df=china, h=15)

plt.figure(figsize=(10,5))

plt.plot(china["ds"], china["y"], label="Observed", linewidth=2)
plt.plot(fc_china["ds"], fc_china["ETS"], label="ETS", linewidth=2)
plt.plot(fc_damped["ds"], fc_damped["Damped_ETS"], label="Damped ETS", linewidth=2)

plt.title("Chinese GDP Forecast Comparison")
plt.xlabel("Year")
plt.ylabel("GDP")

plt.legend()
plt.tight_layout()
plt.show()

The standard ETS model and the damped trend model may appear similar for the immediate future, but as we extend the forecast, the two begin to diverge. In the ETS model, the residual momentum caused by the long-term GDP boom continues to drive the forecast up, indicating extremely high growth rates in the future.

In the damped trend model, the trend is damped as we extend the forecast. This means that the forecast still increases, but at a slower rate. This is more realistic, as it takes into account the fact that the extremely high growth rate will not continue indefinitely.

Box-Cox Transformation

import numpy as np
from scipy.stats import boxcox
from statsforecast import StatsForecast
from statsforecast.models import AutoETS
import matplotlib.pyplot as plt

china_bc = china.copy()
china_bc["y_bc"], lam = boxcox(china_bc["y"])

china_bc_df = china_bc[["unique_id", "ds"]].copy()
china_bc_df["y"] = china_bc["y_bc"]

sf_bc = StatsForecast(
    models=[AutoETS(model="ZZZ", alias="BoxCox_ETS")],
    freq="YE"
)

fc_bc = sf_bc.forecast(df=china_bc_df, h=15)

if abs(lam) < 1e-8:
    fc_bc["BoxCox_ETS_inv"] = np.exp(fc_bc["BoxCox_ETS"])
else:
    fc_bc["BoxCox_ETS_inv"] = np.power(lam * fc_bc["BoxCox_ETS"] + 1, 1 / lam)

plt.figure(figsize=(10, 5))

plt.plot(china["ds"], china["y"], label="Observed", linewidth=2)
plt.plot(fc_china["ds"], fc_china["ETS"], label="ETS", linewidth=2)
plt.plot(fc_damped["ds"], fc_damped["Damped_ETS"], label="Damped ETS", linewidth=2)
plt.plot(fc_bc["ds"], fc_bc["BoxCox_ETS_inv"], label="Box-Cox ETS", linewidth=2)

plt.title("Chinese GDP Forecast Comparison")
plt.xlabel("Year")
plt.ylabel("GDP")
plt.legend()
plt.tight_layout()
plt.show()

print("Estimated Box-Cox lambda:", round(lam, 4))

Estimated Box-Cox lambda: -0.1366

A Box-Cox transformation was used for the Chinese economic series based on GDP before the application of the ETS model. The value of the transformed series’ \(\lambda\) was -0.1366. Essentially, this is a log transformation. This type of transformation is often used for economic series with high rates of growth. It helps to normalize the variance while also limiting the effects of extreme values.

Finally, the ETS model was applied to the transformed series. Then the forecasts were reversed to the original series. With this method, the forecasts increase very quickly as the forecast horizon increases. This is due to the fact that the series for the Chinese economic series based on GDP increases exponentially. Then the inverse mapping is applied to the forecasts.

Comparing the three methods, we see that the standard ETS model maintains the high growth pattern of the series. With the damped trend model, the long-run forecasts increase moderately. With the Box-Cox method, the forecasts increase very steeply if the series increases exponentially.

Exercise 8.7

import pandas as pd
import matplotlib.pyplot as plt

gas = pd.read_csv("aus_production.csv")

gas["ds"] = pd.to_datetime(gas["ds"])
gas = gas.sort_values("ds")

gas_df = gas[["ds","Gas"]].dropna().copy()
gas_df["unique_id"] = "Gas"
gas_df = gas_df.rename(columns={"Gas":"y"})
gas_df = gas_df[["unique_id","ds","y"]]

plt.figure(figsize=(10,5))
plt.plot(gas_df["ds"], gas_df["y"], linewidth=2)

plt.title("Australian Gas Production")
plt.xlabel("Quarter")
plt.ylabel("Gas Production")

plt.tight_layout()
plt.show()

The data on gas production in Australia follows a simple pattern. Production increases over time, and every year the pattern of seasonality repeats itself.

An important point to note about this data is that the seasonality increases as the level of the data increases. Initially, the peaks and troughs of seasonality are small, while later on, the seasonality becomes larger. In other words, the seasonality increases as the level of the data increases.

Since the seasonality increases as the level of the data increases, a multiplicative model of seasonality would be more appropriate. This is because the seasonality can increase as the level of the data increases.

from statsforecast import StatsForecast
from statsforecast.models import AutoETS

sf_gas = StatsForecast(
    models=[AutoETS(model="ZZZ", alias="AutoETS")],
    freq="Q"
)

fc_gas = sf_gas.forecast(df=gas_df, h=8)

plt.figure(figsize=(10,5))

plt.plot(gas_df["ds"], gas_df["y"], label="Observed", linewidth=2)
plt.plot(fc_gas["ds"], fc_gas["AutoETS"], label="Forecast", linewidth=2)

plt.title("Australian Gas Production Forecast")
plt.xlabel("Quarter")
plt.ylabel("Gas Production")

plt.legend()
plt.tight_layout()
plt.show()

As the ETS model predicts the gas output to rise further, it follows the same pattern of rise as the historical data series showed. It also maintains the same pattern of rise and fall as the original series showed.

As the rise and fall are proportional to the level of the series, the model must have multiplicative seasonality. This implies that the rise and fall are expected to increase slightly as the overall output increases.

Overall, the ETS model provides a sensible forecast, reflecting the trend and the seasonality already present in the data series.

Exercise 8.8

Part A

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

aus_retail = pd.read_csv("aus_retail.csv")

print(aus_retail.head())
print(aus_retail.columns)
                          State                                  Industry  \
0  Australian Capital Territory  Cafes, restaurants and catering services   
1  Australian Capital Territory  Cafes, restaurants and catering services   
2  Australian Capital Territory  Cafes, restaurants and catering services   
3  Australian Capital Territory  Cafes, restaurants and catering services   
4  Australian Capital Territory  Cafes, restaurants and catering services   

   Series ID       Month  Turnover  
0  A3349849A  1982-04-01       4.4  
1  A3349849A  1982-05-01       3.4  
2  A3349849A  1982-06-01       3.6  
3  A3349849A  1982-07-01       4.0  
4  A3349849A  1982-08-01       3.6  
Index(['State', 'Industry', 'Series ID', 'Month', 'Turnover'], dtype='object')
series_col = "Series ID" if "Series ID" in aus_retail.columns else "unique_id"
date_col = "Month" if "Month" in aus_retail.columns else ("ds" if "ds" in aus_retail.columns else "Date")
value_col = "Turnover" if "Turnover" in aus_retail.columns else ("y" if "y" in aus_retail.columns else "Sales")

random = np.random.RandomState(34234213)
chosen_id = random.choice(aus_retail[series_col].unique())

retail = aus_retail.loc[aus_retail[series_col] == chosen_id].copy()
retail = retail.rename(columns={series_col: "unique_id", date_col: "ds", value_col: "y"})
retail["ds"] = pd.to_datetime(retail["ds"])
retail = retail[["unique_id", "ds", "y"]].dropna().sort_values("ds")

print("Chosen series:", retail["unique_id"].iloc[0])
retail.head()
Chosen series: A3349721R
unique_id ds y
25821 A3349721R 1982-04-01 161.8
25822 A3349721R 1982-05-01 158.7
25823 A3349721R 1982-06-01 166.6
25824 A3349721R 1982-07-01 172.9
25825 A3349721R 1982-08-01 165.9
plt.figure(figsize=(10, 5))
plt.plot(retail["ds"], retail["y"], linewidth=2)
plt.title(f"Retail Series: {retail['unique_id'].iloc[0]}")
plt.xlabel("Date")
plt.ylabel("Turnover")
plt.tight_layout()
plt.show()

As the seasonal variations in this series of retail sales increase in proportion with the size of the overall data, the series is said to have multiplicative seasonality. Though in the initial years the seasonal highs and lows are minimal, they increase significantly as the series progresses. Thus, the size of the seasonal variations changes in proportion with the overall level of the series. Accordingly, the multiplicative model is more appropriate for this retail time series than the additive model.

Part B

from statsforecast import StatsForecast
from statsforecast.models import AutoETS
import matplotlib.pyplot as plt

sf_hw = StatsForecast(
    models=[
        AutoETS(model="MAM", season_length=12, alias="HW_Mult"),
        AutoETS(model="MAM", season_length=12, damped=True, alias="HW_Mult_Damped"),
    ],
    freq="MS"
)

fc_hw = sf_hw.forecast(df=retail, h=24)

plt.figure(figsize=(10,5))

plt.plot(retail["ds"], retail["y"], label="Observed", linewidth=2)
plt.plot(fc_hw["ds"], fc_hw["HW_Mult"], label="HW Multiplicative", linewidth=2)
plt.plot(fc_hw["ds"], fc_hw["HW_Mult_Damped"], label="HW Multiplicative Damped", linewidth=2)

plt.title("Holt-Winters Multiplicative Forecasts")
plt.xlabel("Date")
plt.ylabel("Turnover")
plt.legend()
plt.tight_layout()
plt.show()

The Holt-Winters multiplicative models were used for the retail data with a 12-month seasonal cycle. This approach allows the models to detect the rising trend and the seasonal fluctuations present in the series.

The two models were the standard multiplicative Holt-Winters and the damped trend version of the Holt-Winters model. Both models were able to detect the seasonality and the overall rising trend in the series. However, the damped trend version of the model adjusts the long-term growth downwards, resulting in a more conservative growth path than the standard multiplicative version.

Part C

from utilsforecast.evaluation import evaluate
from utilsforecast.losses import rmse

sf_hw = StatsForecast(
    models=[
        AutoETS(model="MAM", season_length=12, alias="HW_Mult"),
        AutoETS(model="MAM", season_length=12, damped=True, alias="HW_Mult_Damped"),
    ],
    freq="MS"
)

sf_hw.forecast(df=retail, h=24, fitted=True)

fitted_hw = sf_hw.forecast_fitted_values()

rmse_hw = evaluate(fitted_hw, metrics=[rmse])

rmse_hw

print("HW Multiplicative RMSE =", rmse_hw["HW_Mult"].iloc[0])
print("HW Multiplicative Damped RMSE =", rmse_hw["HW_Mult_Damped"].iloc[0])
HW Multiplicative RMSE = 31.71313418576045
HW Multiplicative Damped RMSE = 35.65843732128966

We used a comparison of one-step ahead forecast RMSEs for the following models: the standard multiplicative form of the Holt-Winters method and its damped counterpart. The standard multiplicative form of the Holt-Winters method yielded an RMSE of 31.71, while its damped counterpart yielded an RMSE of 35.66. Therefore, the undamped form of the Holt-Winters method is preferable.

Part D

from statsmodels.graphics.tsaplots import plot_acf
import matplotlib.pyplot as plt

fitted_hw = sf_hw.forecast_fitted_values()

fitted_best = fitted_hw[["unique_id", "ds", "HW_Mult"]].copy()

residuals = retail.merge(fitted_best, on=["unique_id", "ds"], how="left")

residuals["resid"] = residuals["y"] - residuals["HW_Mult"]

residuals = residuals.dropna(subset=["resid"]).copy()

print(residuals.head())
print(residuals["resid"].describe())
print("Number of residuals:", residuals["resid"].notna().sum())
   unique_id         ds      y     HW_Mult     resid
0  A3349721R 1982-04-01  161.8  156.683101  5.116899
1  A3349721R 1982-05-01  158.7  160.099418 -1.399418
2  A3349721R 1982-06-01  166.6  157.259629  9.340371
3  A3349721R 1982-07-01  172.9  167.871052  5.028948
4  A3349721R 1982-08-01  165.9  170.076865 -4.176865
count    441.000000
mean       3.225776
std       31.584479
min      -89.416941
25%      -12.019447
50%        2.529823
75%       16.019481
max      117.041271
Name: resid, dtype: float64
Number of residuals: 441
plt.figure(figsize=(10,4))
plt.plot(residuals["ds"], residuals["resid"], linewidth=1.2)
plt.axhline(0, linestyle="--")
plt.title("Residuals from Holt-Winters Multiplicative Model")
plt.xlabel("Date")
plt.ylabel("Residual")
plt.tight_layout()
plt.show()

plt.figure(figsize=(8,4))
plt.hist(residuals["resid"], bins=25)
plt.title("Residual Distribution")
plt.xlabel("Residual")
plt.ylabel("Frequency")
plt.tight_layout()
plt.show()

plt.figure(figsize=(8,4))
plot_acf(residuals["resid"], lags=40)
plt.tight_layout()
plt.show()
<Figure size 768x384 with 0 Axes>

Residual diagnostics for the Holt-Winters multiplicative model were performed. The plot of the residuals over time shows the values moving in and out of zero, with the impression that the values are increasing a little towards the end of the time series. The distribution of the residuals in the histogram is fairly symmetric and centered at zero, which indicates that the errors are normally distributed.

However, the results from the ACF plot are quite different. Several significant values are observed at the early lags, beyond the confidence interval. This indicates that the residuals are not pure white noise; they have patterns in them.

Part E

from statsforecast import StatsForecast
from statsforecast.models import AutoETS, SeasonalNaive
from utilsforecast.evaluation import evaluate
from utilsforecast.losses import rmse

train = retail.loc[retail["ds"] < "2011-01-01"].copy()
test = retail.loc[retail["ds"] >= "2011-01-01"].copy()

sf_test = StatsForecast(
    models=[
        AutoETS(model="MAM", season_length=12, alias="HW_Mult"),
        SeasonalNaive(season_length=12, alias="SeasonalNaive")
    ],
    freq="MS"
)

fc_test = sf_test.forecast(df=train, h=len(test))

fc_test["y"] = test["y"].values

test_eval = evaluate(fc_test, train_df=train, metrics=[rmse])

test_eval
unique_id metric HW_Mult SeasonalNaive
0 A3349721R rmse 334.291573 361.598804
plt.figure(figsize=(10,5))

plt.plot(train["ds"], train["y"], label="Train", linewidth=2)
plt.plot(test["ds"], test["y"], label="Test", linewidth=2)
plt.plot(fc_test["ds"], fc_test["HW_Mult"], label="HW Multiplicative", linewidth=2)
plt.plot(fc_test["ds"], fc_test["SeasonalNaive"], label="Seasonal Naive", linewidth=2)

plt.title("Test Set Forecast Comparison")
plt.xlabel("Date")
plt.ylabel("Turnover")
plt.legend()
plt.tight_layout()
plt.show()

The retail series was split into a training set up to 2010 and a test set from 2011. The forecasts were generated using two methods: the Holt-Winters multiplicative method and the seasonal naive method. The accuracy of the models was determined based on the RMSE of the test set.

For the test set, the Holt-Winters method gave an RMSE of 334.29, while the seasonal naive method gave an RMSE of 361.60. Therefore, the Holt-Winters method has a lower RMSE and hence a better forecasting accuracy for the retail series.

This implies that the Holt-Winters multiplicative method has a better fit for the trend and seasonal components of the series than the seasonal naive method.

Exercise 8.9

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from scipy.stats import boxcox
from statsmodels.tsa.seasonal import STL
from statsforecast import StatsForecast
from statsforecast.models import AutoETS
from utilsforecast.evaluation import evaluate
from utilsforecast.losses import rmse

train = retail.loc[retail["ds"] < "2011-01-01"].copy()
test = retail.loc[retail["ds"] >= "2011-01-01"].copy()

train_bc = train.copy()
train_bc["y_bc"], lam = boxcox(train_bc["y"])

print("Estimated Box-Cox lambda:", round(lam, 4))
Estimated Box-Cox lambda: 0.1359
stl = STL(train_bc["y_bc"], period=12, robust=True)
stl_fit = stl.fit()

train_bc["seasonal"] = stl_fit.seasonal
train_bc["trend"] = stl_fit.trend
train_bc["remainder"] = stl_fit.resid
train_bc["season_adjusted"] = train_bc["y_bc"] - train_bc["seasonal"]

plt.figure(figsize=(10, 6))
plt.subplot(4, 1, 1)
plt.plot(train_bc["ds"], train_bc["y_bc"])
plt.title("Box-Cox Transformed Series")

plt.subplot(4, 1, 2)
plt.plot(train_bc["ds"], train_bc["seasonal"])
plt.title("Seasonal Component")

plt.subplot(4, 1, 3)
plt.plot(train_bc["ds"], train_bc["trend"])
plt.title("Trend Component")

plt.subplot(4, 1, 4)
plt.plot(train_bc["ds"], train_bc["remainder"])
plt.title("Remainder")

plt.tight_layout()
plt.show()

train_sa = train_bc[["unique_id", "ds"]].copy()
train_sa["y"] = train_bc["season_adjusted"]

sf_stl = StatsForecast(
    models=[AutoETS(model="ZZZ", alias="ETS_STL")],
    freq="MS"
)

fc_sa = sf_stl.forecast(df=train_sa, h=len(test))
fc_sa.head()
unique_id ds ETS_STL
0 A3349721R 2011-01-01 12.971735
1 A3349721R 2011-02-01 12.987117
2 A3349721R 2011-03-01 13.001487
3 A3349721R 2011-04-01 13.014910
4 A3349721R 2011-05-01 13.027450
last_season = train_bc["seasonal"].iloc[-12:].to_numpy()

seasonal_future = np.resize(last_season, len(test))

fc_stl = fc_sa.copy()
fc_stl["seasonal"] = seasonal_future
fc_stl["y_bc_forecast"] = fc_stl["ETS_STL"] + fc_stl["seasonal"]
if abs(lam) < 1e-8:
    fc_stl["STL_ETS_Forecast"] = np.exp(fc_stl["y_bc_forecast"])
else:
    fc_stl["STL_ETS_Forecast"] = np.power(lam * fc_stl["y_bc_forecast"] + 1, 1 / lam)

fc_stl["y"] = test["y"].values
fc_stl.head()
unique_id ds ETS_STL seasonal y_bc_forecast STL_ETS_Forecast y
0 A3349721R 2011-01-01 12.971735 0.054343 13.026078 1803.521901 1806.0
1 A3349721R 2011-02-01 12.987117 -0.259145 12.727972 1618.243672 1622.3
2 A3349721R 2011-03-01 13.001487 0.011461 13.012948 1794.991615 1749.7
3 A3349721R 2011-04-01 13.014910 -0.029225 12.985685 1777.390809 1761.1
4 A3349721R 2011-05-01 13.027450 -0.018499 13.008950 1792.401376 1740.7
stl_eval = evaluate(
    fc_stl[["unique_id", "ds", "y", "STL_ETS_Forecast"]],
    train_df=train,
    metrics=[rmse]
)

stl_eval
unique_id metric STL_ETS_Forecast
0 A3349721R rmse 195.450355
print("STL + Box-Cox + ETS RMSE =", stl_eval["STL_ETS_Forecast"].iloc[0])
print("Best previous HW Multiplicative RMSE = 334.291573")
STL + Box-Cox + ETS RMSE = 195.45035479544845
Best previous HW Multiplicative RMSE = 334.291573
plt.figure(figsize=(10,5))

plt.plot(train["ds"], train["y"], label="Train", linewidth=2)
plt.plot(test["ds"], test["y"], label="Test", linewidth=2)
plt.plot(fc_stl["ds"], fc_stl["STL_ETS_Forecast"], label="STL + Box-Cox + ETS", linewidth=2)

plt.title("STL + Box-Cox + ETS Forecast")
plt.xlabel("Date")
plt.ylabel("Turnover")
plt.legend()
plt.tight_layout()
plt.show()

For this same retail series, we have performed the STL decomposition on the training data after applying the Box-Cox transformation, using a lambda of 0.1359. We have removed the seasonality component from the data and then performed the ETS model on the seasonally adjusted data. After that, we have proceeded to forecast the data for the test period. We have readded the seasonality component and performed the inverse transformation back to the original scale.

The STL + Box-Cox + ETS strategy has produced a test RMSE of 195.45, which is significantly lower than the best model so far—the Holt-Winters multiplicative method—which produced a test RMSE of 334.29.

This proves that the STL strategy provides much better forecast results for this retail series. This is because the seasonality component has been separated first and then the trend and the residuals have been handled by the ETS model.