Mathew K Analytics

Lesson 42 · Python For Time Series

Using Multilayer Perceptrons for Time Series Forecasting in Christian Analytics

In this lesson, you will learn what ensemble forecasting is and why it is so powerful for predicting things like sales, weather, or daily cases. We will use…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb
 

Welcome to Ensemble Forecasting for Time Series#

In this lesson, you will learn what ensemble forecasting is and why it is so powerful for predicting things like sales, weather, or daily cases.

We will use real-world data and build a simple ensemble model together.

No prior experience is needed let us explore time series and forecasting in Python step by step!

What is a Time Series?#

A time series is just a list of numbers arranged in order over time.

For example, the number of airline passengers each month or daily temperatures.

Time series forecasting tries to predict what will happen next!

import warnings; warnings.filterwarnings("ignore")

# Data setup
import pandas as pd
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv"
data = pd.read_csv(url, parse_dates=["Month"])
print("Shape:", data.shape)
print(data.head())
Shape: (144, 2)
       Month  Passengers
0 1949-01-01         112
1 1949-02-01         118
2 1949-03-01         132
3 1949-04-01         129
4 1949-05-01         121
# Visualize the time series
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 4))
plt.plot(data["Month"], data["Passengers"], label="# Passengers")
plt.xlabel("Month")
plt.ylabel("Passengers")
plt.title("Airline Passengers Over Time")
plt.legend()
plt.show()
No description has been provided for this image

Why Use Ensembles?#

An ensemble combines predictions from several different models.

This can give us more accurate and stable forecasts.

Each model may make different mistakes, but together, their average prediction is often better!

That is like asking many people to guess the average guess is usually close to the truth.

# Prepare the data for modeling
data["Passengers"] = data["Passengers"].astype(float)
series = data["Passengers"]
train = series[:-12]
test = series[-12:]
print("Train size:", len(train), "Test size:", len(test))
Train size: 132 Test size: 12
# Naive Forecast: predicts last observed value
def naive_forecast(train, steps):
    return [train.iloc[-1]] * steps

pred_naive = naive_forecast(train, len(test))
print("Naive forecast for next 3 months:", pred_naive[:3])
Naive forecast for next 3 months: [405.0, 405.0, 405.0]
# Moving Average Forecast: uses last 3 values
def moving_average_forecast(train, steps, window=3):
    avg = train.iloc[-window:].mean()
    return [avg] * steps

pred_ma = moving_average_forecast(train, len(test))
print("Moving average forecast for next 3 months:", pred_ma[:3])
Moving average forecast for next 3 months: [391.3333333333333, 391.3333333333333, 391.3333333333333]
# Simple Exponential Smoothing
from statsmodels.tsa.holtwinters import SimpleExpSmoothing
model_exp = SimpleExpSmoothing(train).fit()
pred_exp = model_exp.forecast(len(test))
print("Exponential smoothing forecast for next 3 months:", pred_exp[:3].round())
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[6], line 2
      1 # Simple Exponential Smoothing
----> 2 from statsmodels.tsa.holtwinters import SimpleExpSmoothing
      3 model_exp = SimpleExpSmoothing(train).fit()
      4 pred_exp = model_exp.forecast(len(test))

ModuleNotFoundError: No module named 'statsmodels'
# Compare models visually
plt.figure(figsize=(10,4))
plt.plot(test.index, test.values, label="Actual", marker="o")
plt.plot(test.index, pred_naive, label="Naive", linestyle="--")
plt.plot(test.index, pred_ma, label="Moving Avg", linestyle=":")
plt.plot(test.index, pred_exp, label="Exp Smoothing", linestyle="-.")
plt.title("Comparing Individual Models")
plt.legend()
plt.show()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 6
      4 plt.plot(test.index, pred_naive, label="Naive", linestyle="--")
      5 plt.plot(test.index, pred_ma, label="Moving Avg", linestyle=":")
----> 6 plt.plot(test.index, pred_exp, label="Exp Smoothing", linestyle="-.")
      7 plt.title("Comparing Individual Models")
      8 plt.legend()

NameError: name 'pred_exp' is not defined
No description has been provided for this image

What is an Ensemble?#

Now for the fun: let us combine our models!

An ensemble will simply average the predictions from each model.

By letting models "vote", we hope to reduce errors from any one model.

# Create a simple ensemble by averaging
import numpy as np
ensemble_pred = np.mean([pred_naive, pred_ma, pred_exp], axis=0)
print("First 3 ensemble forecasts:", ensemble_pred[:3].round())
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 3
      1 # Create a simple ensemble by averaging
      2 import numpy as np
----> 3 ensemble_pred = np.mean([pred_naive, pred_ma, pred_exp], axis=0)
      4 print("First 3 ensemble forecasts:", ensemble_pred[:3].round())

NameError: name 'pred_exp' is not defined
# Visualize the ensemble against real data
plt.figure(figsize=(10,4))
plt.plot(test.index, test.values, label="Actual", marker="o")
plt.plot(test.index, ensemble_pred, label="Ensemble", color="red", linewidth=2)
plt.title("Ensemble Model vs Actual Data")
plt.legend()
plt.show()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 4
      2 plt.figure(figsize=(10,4))
      3 plt.plot(test.index, test.values, label="Actual", marker="o")
----> 4 plt.plot(test.index, ensemble_pred, label="Ensemble", color="red", linewidth=2)
      5 plt.title("Ensemble Model vs Actual Data")
      6 plt.legend()

NameError: name 'ensemble_pred' is not defined
No description has been provided for this image
# Calculate errors to compare models
from sklearn.metrics import mean_squared_error

def rmse(y_true, y_pred):
    return mean_squared_error(y_true, y_pred, squared=False)

print("Naive RMSE:", rmse(test, pred_naive).round(2))
print("Moving Avg RMSE:", rmse(test, pred_ma).round(2))
print("Exp Smoothing RMSE:", rmse(test, pred_exp).round(2))
print("Ensemble RMSE:", rmse(test, ensemble_pred).round(2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 7
      4 def rmse(y_true, y_pred):
      5     return mean_squared_error(y_true, y_pred, squared=False)
----> 7 print("Naive RMSE:", rmse(test, pred_naive).round(2))
      8 print("Moving Avg RMSE:", rmse(test, pred_ma).round(2))
      9 print("Exp Smoothing RMSE:", rmse(test, pred_exp).round(2))

Cell In[10], line 5, in rmse(y_true, y_pred)
      4 def rmse(y_true, y_pred):
----> 5     return mean_squared_error(y_true, y_pred, squared=False)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\sklearn\utils\_param_validation.py:196, in validate_params.<locals>.decorator.<locals>.wrapper(*args, **kwargs)
    193 func_sig = signature(func)
    195 # Map *args/**kwargs to the function signature
--> 196 params = func_sig.bind(*args, **kwargs)
    197 params.apply_defaults()
    199 # ignore self/cls and positional/keyword markers

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\inspect.py:3259, in Signature.bind(self, *args, **kwargs)
   3254 def bind(self, /, *args, **kwargs):
   3255     """Get a BoundArguments object, that maps the passed `args`
   3256     and `kwargs` to the function's signature.  Raises `TypeError`
   3257     if the passed arguments can not be bound.
   3258     """
-> 3259     return self._bind(args, kwargs)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\inspect.py:3248, in Signature._bind(self, args, kwargs, partial)
   3246         arguments[kwargs_param.name] = kwargs
   3247     else:
-> 3248         raise TypeError(
   3249             'got an unexpected keyword argument {arg!r}'.format(
   3250                 arg=next(iter(kwargs))))
   3252 return self._bound_arguments_cls(self, arguments)

TypeError: got an unexpected keyword argument 'squared'

Real-World Example: Shampoo Sales Ensemble#

Let us see ensemble forecasting at work on a different real-world dataset: shampoo sales.

Ensembles are especially useful in business for planning and resources.

# Load Shampoo Sales Data
url2 = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/shampoo.csv"
sales = pd.read_csv(url2, parse_dates=["Month"])
sales["Sales"] = sales["Sales"].astype(float)
plt.figure(figsize=(10,4))
plt.plot(sales["Month"], sales["Sales"], label="Shampoo Sales")
plt.title("Monthly Shampoo Sales")
plt.xlabel("Month")
plt.ylabel("Sales Volume")
plt.legend()
plt.show()
No description has been provided for this image
# Practice: Build your own simple ensemble
practice_series = sales["Sales"]
train_s = practice_series[:-6]
test_s = practice_series[-6:]

naive_s = naive_forecast(train_s, len(test_s))
ma_s = moving_average_forecast(train_s, len(test_s), window=2)
exp_s = SimpleExpSmoothing(train_s).fit().forecast(len(test_s))

user_ensemble = np.mean([naive_s, ma_s, exp_s], axis=0)
plt.figure(figsize=(10,4))
plt.plot(test_s.index, test_s.values, label="Actual", marker="o")
plt.plot(test_s.index, user_ensemble, label="Your Ensemble", color="green")
plt.title("Your Ensemble on Shampoo Sales")
plt.legend()
plt.show()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 8
      6 naive_s = naive_forecast(train_s, len(test_s))
      7 ma_s = moving_average_forecast(train_s, len(test_s), window=2)
----> 8 exp_s = SimpleExpSmoothing(train_s).fit().forecast(len(test_s))
     10 user_ensemble = np.mean([naive_s, ma_s, exp_s], axis=0)
     11 plt.figure(figsize=(10,4))

NameError: name 'SimpleExpSmoothing' is not defined
# Challenge: Try user input for forecast length!
user_steps = int(input("How many future months would you like to forecast? (1-6): "))
if 1 <= user_steps <= 6:
    user_pred = np.mean([
        naive_forecast(practice_series[:-user_steps], user_steps),
        moving_average_forecast(practice_series[:-user_steps], user_steps, window=2),
        SimpleExpSmoothing(practice_series[:-user_steps]).fit().forecast(user_steps)
    ], axis=0)
    print("Your ensemble for next", user_steps, "months:", user_pred.round())
else:
    print("Please enter a number from 1 to 6.")
    
    ])
  Cell In[13], line 13
    ])
    ^
SyntaxError: unmatched ']'
bm
## Best Practices Checkup

- Always use a baseline model like naive or moving average.
- Check your splits: do not peek at future data during training.
- Compare models with the same error measure (like RMSE).
- Visualize predictions; not just numbers.
- Try different ensemble mixes and look for improvement.
  Cell In[14], line 4
    - Always use a baseline model like naive or moving average.
             ^
SyntaxError: invalid syntax
# Troubleshooting tips
try:
    result = moving_average_forecast(train, -5)
except Exception as e:
    print("Error:", str(e))

# Normally, steps must be positive!
# Extra tip: Weighted ensembles
weights = [0.2, 0.2, 0.6]
weighted_ensemble = np.average([pred_naive, pred_ma, pred_exp], axis=0, weights=weights)
print("Weighted ensemble (more weight on exp smoothing):", weighted_ensemble[:3].round())
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[16], line 3
      1 # Extra tip: Weighted ensembles
      2 weights = [0.2, 0.2, 0.6]
----> 3 weighted_ensemble = np.average([pred_naive, pred_ma, pred_exp], axis=0, weights=weights)
      4 print("Weighted ensemble (more weight on exp smoothing):", weighted_ensemble[:3].round())

NameError: name 'pred_exp' is not defined

Challenge Exercise#

  • Try making your own ensemble including at least three different models.
  • Test on both airline and shampoo data.
  • Try using a different size for your moving average window.

Can your new ensemble beat the best single model?

Recap: What Have You Learned?#

  • What a time series is
  • How to create and compare basic forecasting models
  • How to build an ensemble by combining forecasts
  • Why ensembles are often more accurate
  • Best practices for reliable time series forecasting

Keep Learning!#

If you enjoyed this lesson, give us a like and subscribe for more beginner-friendly Python tutorials.

Let us know in the comments what topics you want to see next!

Happy forecasting!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.