#!/usr/bin/env python
# coding: utf-8
# # Smoothing
#
#
# ## Introduction
#
# In the previous lectures, we learned that:
# - A time series consists of measurements characterized by temporal dependencies.
# - The measurements are (usually) collected at equally-spaced intervals.
# - A time series can be decomposed into trend, seasonality, and residuals.
# - Many time series models require the data to be stationary in order to make forecasts.
# - In this lecture, we will build upon that knowledge and explore another important concept called **smoothing**.
# - In particular, we will cover:
# 1. An introduction to smoothing and why it is necessary.
# 2. Common smoothing techniques.
# 3. How to smooth time series data with Python and generate forecasts.
# In[1]:
import sys
# Install dependencies if the notebook is running in Colab
if 'google.colab' in sys.modules:
get_ipython().system('pip install -U -qq tsa-course')
# In[2]:
# Imports
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import requests
from io import BytesIO
from tsa_course.lecture2 import run_sequence_plot
np.random.seed(0) # reproducibility
# ---
# ## Smoothing
# - A data collection process is often affected by noise.
# - If too strong, the noise can conceal useful patterns in the data.
# - Smoothing is a well-known and often-used technique to recover those patterns by *filtering out noise*.
# - It can also be used to *make forecasts* by projecting the recovered patterns into the future.
# - We will explore two important techniques to do smoothing:
# 1. Simple smoothing.
# 2. Exponential smoothing.
# - We start by generating some stationary data.
# - We discussed the importance of visually inspecting the time series with a run-sequence plot.
# - So, we will also define the `run_sequence_plot` function to visualize our data.
# In[3]:
# Generate stationary data
time = np.arange(100)
stationary = np.random.normal(loc=0, scale=1.0, size=len(time))
# In[4]:
run_sequence_plot(time, stationary, title="Stationary time series");
#
# ---
# ## Simple smoothing techniques
# - There are many techniques for smoothing data.
# - The most simple ones are:
# 1. Simple average
# 2. Moving average
# 3. Weighted moving average
# ### Simple average
#
# - Simple average is the most basic technique.
# - Consider the stationary data above.
# - The most conservative way to represent it is through its **mean**.
# - The mean can be used to predict the future values of the time series.
# - This type of representation is called *simple average*.
# In[5]:
# find mean of series
stationary_time_series_avg = np.mean(stationary)
# create array composed of mean value and equal to length of time array
sts_avg = np.full(shape=len(time), fill_value=stationary_time_series_avg, dtype='float')
# In[6]:
ax = run_sequence_plot(time, stationary, title="Stationary Data")
ax.plot(time, sts_avg, 'tab:red', label="mean")
plt.legend();
# ### Mean squared error (MSE)
#
# - The approximation with the mean seems reasonable in this case.
# - In general we want to measure how far off our estimate is from reality.
# - A common way of doing it is by calculating the *Mean Squared Error* (MSE)
#
# $$MSE = \frac{1}{T}\sum_{t=1}^{T} (X(t) - \hat{X}(t))^2$$
#
# - where $X(t)$ and $\hat{X}(t)$ are the true and estimated values at time $t$, respectively.
# **Example**
#
# - Let's consider the following example.
# - Say we have a time series of observed values $X = [0, 1, 3, 2]$.
# - The predictions given by our model are $\hat{X} = [1, 1, 2, 4]$.
# - We calculate the MSE as:
#
# $$\frac{(0-1)^{2} + (1-1)^{2} + (3-2)^{2} + (2-4)^{2}}{4} = 1.5$$
# - Say we had another model that give us the estimate $\hat{X} = [0, 0, 1, 0]$.
# - The MSE in this case is:
#
# $$\frac{0^{2} + 1^{2} + 2^{2} + 2^{2}}{4} = 2.25$$
# - The MSE allows to compare different estimates to see which is best.
# - In this case, the first model gives us a better estimate than the second one.
# - This idea of measuring how a model performs is important in machine learning and we will use it often in this course.
# - Let's create a function to calculate MSE that we will use as we go forward.
# In[7]:
def mse(observations, estimates):
"""
INPUT:
observations - numpy array of values indicating observed values
estimates - numpy array of values indicating an estimate of values
OUTPUT:
Mean Square Error value
"""
# check arg types
assert type(observations) == type(np.array([])), "'observations' must be a numpy array"
assert type(estimates) == type(np.array([])), "'estimates' must be a numpy array"
# check length of arrays equal
assert len(observations) == len(estimates), "Arrays must be of equal length"
# calculations
difference = observations - estimates
sq_diff = difference ** 2
mse = np.mean(sq_diff)
return mse
# Let's test the ``mse`` function.
# In[8]:
zeros = mse(np.array([0, 1, 3, 2]), np.array([1, 1, 2, 4]))
print(zeros)
# In[9]:
ones = mse(np.array([0, 1, 3, 2]), np.array([0, 0, 1, 0]))
print(ones)
# - Next, we add a *trend* to our stationary time series.
# In[10]:
trend = (time * 2.0) + stationary
run_sequence_plot(time, trend, title="Time series with trend");
# - Suppose we use again the simple average to represent the time series.
# In[11]:
# find mean of series
trend_time_series_avg = np.mean(trend)
# create array of mean value equal to length of time array
avg_trend = np.full(shape=len(time), fill_value=trend_time_series_avg, dtype='float')
run_sequence_plot(time, trend, title="Time series with trend")
plt.plot(time, avg_trend, 'tab:red', label="mean")
plt.legend();
# - Clearly, the simple average does not work well in this case.
# - We must find other ways to capture the underlying pattern in the data.
# - We start with something called a *moving average*.
# ### Moving Average (MA)
#
# - Moving average has a greater sensitivity than the simple average to local changes in the data.
# - The easiest way to understand moving average is by example.
# - Say we have the following values:
#
#
# - The first step is to select a window size.
# - We'll arbitrarily choose a size of 3.
# - Then, we start computing the average for the first three values and store the result.
# - We then slide the window by one and calculate the average of the next three values.
# - We repeat this process until we reach the final observed value.
#
#
# - Now, let's define a function to perform smoothing with the MA.
# - Then, we compare the MSE obtained from applying simple and moving average on the data with trend.
# In[12]:
def moving_average(observations, window=3, forecast=False):
cumulative_sum = np.cumsum(observations, dtype=float)
cumulative_sum[window:] = cumulative_sum[window:] - cumulative_sum[:-window]
ma = cumulative_sum[window - 1:] / window
if forecast:
observations = np.append(observations, np.nan)
ma_forecast = np.insert(ma, 0, np.nan*np.ones(window))
return observations, ma_forecast
else:
return ma
# In[13]:
MA_trend = moving_average(trend, window=3)
print(f"MSE:\n--------\nsimple average: {mse(trend, avg_trend):.2f}\nmoving_average: {mse(trend[2:], MA_trend):.2f}")
# - Clearly, the MA manages to pick up much better the trend data.
# - We can also plot the result against the actual time series.
# In[14]:
run_sequence_plot(time, trend, title="Time series with trend")
plt.plot(time[1:-1], MA_trend, 'tab:red', label="MA")
plt.legend();
# - Note that the output of the MA is shorter than the original data.
# - The reason is that the moving window is not centered on the first and last elements of the time series.
# - Given a window of size $P$, the MA will be $P-1$ steps shorter than the original time series.
# In[15]:
x = np.arange(20)
print(len(moving_average(x, window=9)))
# - Now, we try the MA on a periodic time series, which could represent a seasonality.
# In[16]:
seasonality = 10 + np.sin(time) * 10 + stationary
MA_seasonality = moving_average(seasonality, window=3)
# In[17]:
run_sequence_plot(time, seasonality, title="Seasonality")
plt.plot(time[1:-1], MA_seasonality, 'tab:red', label="MA")
plt.legend(loc='upper left');
# - It's not perfect but clearly picks up the periodic pattern.
# - Lastly, let's see how MA handles trend, seasonality, and a bit of noise.
# In[18]:
trend_seasonality = trend + seasonality + stationary
MA_trend_seasonality = moving_average(trend_seasonality, window=3)
# In[19]:
run_sequence_plot(time, trend_seasonality, title="Trend, seasonality, and noise")
plt.plot(time[1:-1], MA_trend_seasonality, 'tab:red', label="MA")
plt.legend(loc='upper left');
# - This method is picking up key patterns in these toy datasets.
# - However, it has several limitations.
# 1. MA assigns equal importance to all values in the window, regardless of their chronological order.
# - For this reason it fails in capturing the often more relevant recent trends.
# 2. MA requires the selection of a specific window size, which can be arbitrary and may not suit all types of data.
# - a small window may lead to noise
# - a too large window could oversmooth the data, missing important short-term fluctuations.
# 3. MA does not adjust for changes in trend or seasonality.
# - This can lead to inaccurate predictions, especially when these components are nonlinear and time-dependent.
# ### Weighted moving average (WMA)
#
# - WMA weights recent observations more than more distant ones.
# - This makes intuitive sense.
# - Think of the stock market: it has been observed that today's price is a good predictor of tomorrow's price.
# - By applying unequal weights to past observations, we can control how much each affects the future forecast.
# - There are many ways to set the weights.
# - For example, we could define them with the following system of equations:
#
# $$
# \begin{cases}
# & w_1 + w_2 + w_3 = 1\\
# & w_2 = (w_1)^2 \\
# & w_3 = (w_1)^3
# \end{cases}
# $$
# - This gives the following weights:
#
# $$
# \begin{aligned}
# & w_1 \approx 0.543 & \text{weights associated to $t-1$}\\
# & w_2 \approx 0.294 & \text{weights associated to $t-2$} \\
# & w_3 \approx 0.16 & \text{weights associated to $t-3$}
# \end{aligned}
# $$
#
#
# **πNote**
#
# - There are numerous methods to find the optimal weighting scheme based on the data and the task at hand.
# - A full discussion is beyond the scope of the lecture.
# ### Forecasting with MA
#
# - Instead of pulling out the inherent pattern within a series, the smoothing functions can be used to create *forecasts*.
# - The forecast for the next time step is computed as follows:
#
# $$\hat{X}(t+1) = \frac{X(t) + X(t-1) + \dots + X(t-P+1)}{P}$$
#
# - where $P$ is the window size of the MA.
# - Let's consider the following time series $X= [1, 2, 4, 8, 16, 32, 64]$.
# - In particular, we apply the smoothing process and use the resulting value as forecast for the next time step.
# - With the MA technique and a window size $P=3$ we get the following forecast.
# In[20]:
x = np.array([1, 2, 4, 8, 16, 32, 64])
ma_x, ma_forecast = moving_average(x, window=3, forecast=True)
# In[57]:
t = np.arange(len(ma_x))
run_sequence_plot(t[:-4], ma_x[:-4], title="Nonlinear data")
plt.plot(t[-5:], ma_x[-5:], 'k', label="True values", linestyle='--')
plt.plot(t, ma_forecast, 'tab:red', label="MA forecast", linestyle='--')
plt.legend(loc='upper left');
# - The result shows that MA is lagging behind the actual signal.
# - In this case, it is not able to keep up with changes in the trend.
# - The lag of MA is also reflected in the forecasts it produces.
# - Let's focus on the [forecasting formula](#forecasting-with-ma) we just defined.
# - While easy to understand, one of its properties may not be obvious.
# - What's the lag associated with this technique?
# - In other words, after how many time steps do we see a local change in the underlying signal?
# - The answer is: $\frac{(P+1)}{2}$.
# - For example, say you're averaging the past 5 values to make the next prediction.
# - Then the forecast value that most closely reflects the current value will appear after $\frac{5+1}{2} = 3$ time steps.
# - Clearly, the lag increases as you increase the window size for averaging.
# - One might reduce the window size to obtain a more responsive model.
# - However, a window size that's too small will chase noise in the data as opposed to extracting the pattern.
# - There is a tradeoff between *responsiveness* and *robustness to noise*.
# - The best answer lies somewhere in between and requires careful tuning to determine which setup is best for a given dataset and problem at hand.
# - Let's make a practical example to show this tradeoff.
# - We will generate some toy data and apply a MA with different window sizes.
# - To make things more clear, we generate again data with trend and seasonality, but we add more noise.
# In[22]:
noisy_noise = np.random.normal(loc=0, scale=8.0, size=len(time))
noisy_trend = time * 2.75
noisy_seasonality = 10 + np.sin(time * 0.25) * 20
noisy_data = noisy_trend + noisy_seasonality + noisy_noise
run_sequence_plot(time, noisy_data, title="Noisy data");
# In[23]:
# Compute MA with different window sizes
lag_2 = moving_average(noisy_data, window=3)
lag_3 = moving_average(noisy_data, window=5)
lag_5 = moving_average(noisy_data, window=9)
lag_10 = moving_average(noisy_data, window=19)
# In[24]:
_, axes = plt.subplots(2,2, figsize=(11,6))
axes[0,0] = run_sequence_plot(time, noisy_data, title="MA with lag 2", ax=axes[0,0])
axes[0,0].plot(time[1:-1], lag_2, color='tab:blue', linewidth=2.5)
axes[0,1] = run_sequence_plot(time, noisy_data, title="MA with lag 3", ax=axes[0,1])
axes[0,1].plot(time[2:-2], lag_3, color='tab:orange', linewidth=2.5)
axes[1,0] = run_sequence_plot(time, noisy_data, title="MA with lag 5", ax=axes[1,0])
axes[1,0].plot(time[4:-4], lag_5, color='tab:green', linewidth=2.5)
axes[1,1] = run_sequence_plot(time, noisy_data, title="MA with lag 10", ax=axes[1,1])
axes[1,1].plot(time[9:-9], lag_10, color='tab:red', linewidth=2.5)
plt.tight_layout();
# - Clearly, the larger the window size the smoother the data.
# - This allows to get rid of the noise but, eventually, also the underlying signal is smoothed out.
# ### Forecasting with WMA
#
# - Next, we use the WMA to generate forecasts.
# - The previous [forecasting formula](#forecasting-with-ma) is modified to weight differently the past observations in the window:
#
# $$\hat{X}(t+1) = \frac{w_1 \cdot X(t) + w_2 \cdot X(t-1) + \dots + w_P \cdot X(t-P+1)}{P}$$
# In[25]:
def weighted_moving_average(observations, weights, forecast=False):
if len(weights) != len(observations[0:len(weights)]):
raise ValueError("Length of weights must match the window size")
# Normalize weights
weights = np.array(weights) / np.sum(weights)
# Initialize the result array
result = np.empty(len(observations)-len(weights)//2)
result[:] = np.nan
# Calculate weighted moving average
for i in range(len(weights)//2, len(result)):
window = observations[i-(len(weights)//2):i+len(weights)//2+1]
result[i] = np.dot(window, weights)
# Handle forecast option
if forecast:
result = np.insert(result, 0, np.nan*np.ones(len(weights)//2+1))
observations = np.append(observations, np.nan*np.ones(len(weights)//2))
return observations, result
else:
return result
# - We repeat the experiment on the same time series $X= [1, 2, 4, 8, 16, 32, 64]$.
# - We will use the set of weights that we defined before, `[0.160, 0.294, 0.543]`.
# In[26]:
weights = np.array([0.160, 0.294, 0.543])
wma_x, wma_forecast = weighted_moving_average(x, weights, forecast=True)
# In[59]:
t = np.arange(len(wma_x))
run_sequence_plot(t[:-4], ma_x[:-4], title="Nonlinear data")
plt.plot(t[-5:], ma_x[-5:], 'k', label="True values", linestyle='--')
plt.plot(t, ma_forecast, 'tab:red', label="MA forecast", linestyle='--')
plt.plot(t, wma_forecast, 'tab:blue', label="WMA forecast", linestyle='--')
plt.legend(loc='upper left');
# - WMA solves some issues of MA.
# - It is more sensitive to local changes.
# - Is more flexible, as it can adjust the importance of different time steps.
# - It does not require a fixed window size as it applies an exponentially decreasing weight to all past observations.
# - However it still struggle in keeping pace with nonlinear and fast-changing trends... There's a significant lag.
#
# ---
# ## Exponential Smoothing
#
# There are three key exponential smoothing techniques:
#
# |Type | Capture trend | Capture seasonality|
# |:----:|:---------------:|:--------------------:|
# |Single Exponential Smoothing | β | β |
# |Double Exponential Smoothing | β
| β |
# |Triple Exponential Smoothing | β
| β
|
# ### Single exponential smoothing
#
# - π‘ Useful if your data lacks trend and seasonality and you want to approximately extract patterns.
# - It is analogous to the WMA we saw previously.
# - The basic formulation is:
#
# $$S(t) = \alpha X(t) + (1-\alpha)S(t-1)$$
#
# - where:
# - $S(t)$ is the smoothed value at time *t*,
# - $\alpha$ is a smoothing constant,
# - $X(t)$ is the value of the series at time *t*.
# Expanding the formula we get:
#
# $$
# \begin{aligned}
# S(0) &= \alpha X(0) \\ S(1) &= \alpha X(1) + (1 - \alpha)S(0) = \alpha X(1) + \alpha(1 - \alpha)X(0) \\
# S(2) & = \alpha X(2) + (1 - \alpha)S(1) = \alpha X(2) + (1 - \alpha)[\alpha X(1) + \alpha(1 - \alpha)X(0)]\\
# & = \alpha X(2) + \alpha(1 - \alpha)X(1) + \alpha(1 - \alpha)^2 X(0)
# \end{aligned}
# $$
#
# The formula for a generic time step $\tau$ is:
#
# $$ S(\tau) = \sum_{t=0}^{\tau} \alpha(1 - \alpha)^{\tau-t}X(t) $$
#
# #### Initialization
#
# - There are many initialization strategies.
# - A simple one is to set $S(0) = X(0)$.
# - Another strategy is to find the mean of the first couple of observations, e.g., $S(0) = \frac{X(0) + X(1) + X(2)}{3}$.
# - Once you've initialized it, you can use the update rule above to calculate all values.
# - In practice, we don't perform this process by hand, but we use the functions in ``statsmodels``.
# #### Setting $\alpha$
# - Choosing the optimal value for $\alpha$ is also done by ``statsmodels``.
# - A solver uses a metric like MSE to find the optimal $\alpha$.
# - Even if we do not choose the value manually, is good to have a basic idea of what's happening under the hood.
# ### Double exponential smoothing
#
# - π‘ It has all the benefits of Single Exponential plus the ability to pickup on trend.
#
# $$
# \begin{aligned}
# S(t) = \alpha X(t) + (1-\alpha) \big(S(t-1) + b(t-1)\big) & \hspace{2cm} \text{Smoothed values of the series} \\
# b(t) = \beta\big(S(t) - S(t-1)\big) + (1 - \beta)b(t-1) & \hspace{2cm} \text{Estimated trend} \\
# \hat{X}(t+1) = S(t) + b(t) & \hspace{2cm} \text{Forecasts}
# \end{aligned}
# $$
#
# - The parameter $\beta \in [0,1]$ controls the decay of the influence of change in the trend.
# **Initialization**
#
# - $S(0) = X(0)$.
# - $b(0) = X(1) - X(0)$.
# - Other initializations are also possible.
#
# **Forecasts beyond 1-step ahead**
# - $\hat{X}(t+\tau) = S(t) + \tau b(t)$.
# ### Triple exponential smoothing
#
# - π‘ It has all the benefits of Double Exponential with the ability to also model the seasonality.
# - It does this by adding a third component that smooths out seasonality of length $L$.
# - There are two variants in the model:
# - additive seasonality,
# - multiplicative seasonality.
# **Additive seasonality model**
#
# - This method is preferred when the seasonal variations are roughly constant through the series.
#
# $$
# \begin{aligned}
# S(t) = \alpha \big(X(t) - c(t-L)\big) + (1-\alpha) \big(S(t-1) + b(t-1)\big) & \hspace{2cm} \text{Smoothed values} \\
# b(t) = \beta\big(S(t) - S(t-1)\big) + (1 - \beta)b(t-1) & \hspace{2cm} \text{Estimated trend} \\
# c(t) = \gamma \big(X(t) - S(t-1) - b(t-1)\big) + (1-\gamma)c(t-L) & \hspace{2cm} \text{Seasonal correction} \\
# \hat{X}(t+\tau) = S(t) + \tau b(t) + c(t-L + 1 + (\tau - 1)\text{mod}(L)) & \hspace{2cm} \text{Forecasts}
# \end{aligned}
# $$
# **Multiplicative seasonality model**
#
# - This method is preferred when the seasonal variations are changing proportional to the level of the series.
#
# $$
# \begin{aligned}
# S(t) = \alpha \frac{X(t)}{c(t-L)} + (1-\alpha) \big(S(t-1) + b(t-1)\big) & \hspace{2cm} \text{Smoothed values} \\
# b(t) = \beta \big(S(t) - S(t-1)\big) + (1 - \beta)b(t-1) & \hspace{2cm} \text{Estimated trend} \\
# c(t) = \gamma \left( \frac{X(t)}{S(t)} \right) + (1-\gamma)c(t-L) & \hspace{2cm} \text{Seasonal correction} \\
# \hat{X}(t+\tau) = \big( S(t) + \tau b(t) \big)c(t-L + 1 + (\tau - 1)\text{mod}(L)) & \hspace{2cm} \text{Forecasts}
# \end{aligned}
# $$
# **Initialization**
#
# - $b(0) = \frac{1}{L} \left( \frac{X(L+1) - X(1)}{L} + \frac{X(L+2) - X(2)}{L} + \dots + \frac{X(L+L) - X(L)}{L} \right)$
# - $c(t) = \frac{1}{N} \sum_{j=1}^N \frac{X(L(j-1)+t)}{A_j}$ for $t=1,2,\dots,L$, where $N$ is the total number of seasonal cycles in the data and $A_j = \frac{\sum_{t=1}^L X(2(j-1)+t)}{L}$
#
# ---
# ## Exponential smoothing in Python
# - Let's now see how to perform smoothing in Python.
# - As we've done in the past, we'll leverage ``statsmodels`` to do the heavy lifting for us.
# - We'll use the same time series with trend and seasionality to compare simple average, single, double, and triple exponential smoothing.
# - We will holdout the last 5 samples from the dataset (i.e., they become the *test set*).
# - We will compare the predictions made by the models with these values.
# In[28]:
# Train/test split
train = trend_seasonality[:-5]
test = trend_seasonality[-5:]
# ### Simple Average
#
# - This is a very crude model to say the least.
# - It can at least be used as a baseline.
# - Any model we try moving forward should do much better than this one.
# In[29]:
# find mean of series
trend_seasonal_avg = np.mean(trend_seasonality)
# create array of mean value equal to length of time array
simple_avg_preds = np.full(shape=len(test), fill_value=trend_seasonal_avg, dtype='float')
# mse
simple_mse = mse(test, simple_avg_preds)
# results
print("Predictions: ", simple_avg_preds)
print("MSE: ", simple_mse)
# In[30]:
ax = run_sequence_plot(time[:-5], train, title=f"Simple average - MSE: {simple_mse:.2f}")
ax.plot(time[-5:], test, color='tab:blue', linestyle="--", label="test")
ax.plot(time[-5:], simple_avg_preds, color='tab:orange', linestyle="--", label="preds");
# ### Single Exponential
# In[31]:
from statsmodels.tsa.api import SimpleExpSmoothing
single = SimpleExpSmoothing(train).fit(optimized=True)
single_preds = single.forecast(len(test))
single_mse = mse(test, single_preds)
print("Predictions: ", single_preds)
print("MSE: ", single_mse)
# In[32]:
ax = run_sequence_plot(time[:-5], train, title=f"Simple exponential smoothing - MSE: {single_mse:.2f}")
ax.plot(time[-5:], test, color='tab:blue', linestyle="--", label="test")
ax.plot(time[-5:], single_preds, color='tab:orange', linestyle="--", label="preds");
# - This is certainly better than the simple average method but it's still pretty crude.
# - Notice how the forecast is just a horizontal line.
# - Single Exponential Smoothing cannot pick up neither trend nor seasonality.
# ### Double Exponential
# In[33]:
from statsmodels.tsa.api import Holt
double = Holt(train).fit(optimized=True)
double_preds = double.forecast(len(test))
double_mse = mse(test, double_preds)
print("Predictions: ", double_preds)
print("MSE: ", double_mse)
# In[34]:
ax = run_sequence_plot(time[:-5], train, title=f"Double exponential smoothing - MSE: {double_mse:.2f}")
ax.plot(time[-5:], test, color='tab:blue', linestyle="--", label="test")
ax.plot(time[-5:], double_preds, color='tab:orange', linestyle="--", label="preds");
# - Double Exponential Smoothing can pickup on trend, which is exactly what we see here.
# - This is a significant leap but no quite yet the prediction we would like to get.
# ### Triple Exponential
# In[35]:
from statsmodels.tsa.api import ExponentialSmoothing
triple = ExponentialSmoothing(train,
trend="additive",
seasonal="additive",
seasonal_periods=13).fit(optimized=True)
triple_preds = triple.forecast(len(test))
triple_mse = mse(test, triple_preds)
print("Predictions: ", triple_preds)
print("MSE: ", triple_mse)
# In[36]:
ax = run_sequence_plot(time[:-5], train, title=f"Triple exponential smoothing - MSE: {triple_mse:.2f}")
ax.plot(time[-5:], test, color='tab:blue', linestyle="--", label="test")
ax.plot(time[-5:], triple_preds, color='tab:orange', linestyle="--", label="preds");
# - Triple Exponential Smoothing pickups trend and seasonality.
# - Clearly, this is the most suitable approach for this data.
# - We can summarize the results in the following table:
# In[37]:
data_dict = {'MSE':[simple_mse, single_mse, double_mse, triple_mse]}
df = pd.DataFrame(data_dict, index=['simple', 'single', 'double', 'triple'])
print(df)
# ---
# ## Summary
#
# In this lecture we learned
#
# 1. What is smoothing and why it is necessary.
# 2. Some common smoothing techniques.
# 3. A basic understanding of how to smooth time series data with Python and generate forecasts.
# ---
# ## Exercises
#
# Load the following two time series.
# In[38]:
# Load the first time series
response = requests.get("https://zenodo.org/records/10897398/files/smoothing_ts1.npy?download=1")
response.raise_for_status()
smoothing_ts1 = np.load(BytesIO(response.content))
print(len(smoothing_ts1))
# Load the second time series
response = requests.get("https://zenodo.org/records/10897398/files/smoothing_ts4_4.npy?download=1")
response.raise_for_status()
smoothing_ts2 = np.load(BytesIO(response.content))
print(len(smoothing_ts2))
# Using on what you learned in this and in the previous lectures, do the following.
#
# 1. Create two time variables called `mytime1` and `mytime2` that starts at 0 and are as long as each dataset.
# 2. Split each dataset into train and test sets (as the test, use the last 5 observations).
# 3. Identify trend and seasonality, if present.
# 4. Identify if trend and/or seasonality are additive or multiplicative, if present.
# 5. Create smoothed model on the train set and use to forecast on the test set.
# 6. Calculate MSE on test data.
# 7. Plot training data, test data, and your model's forecast for each dataset.