Time series forecasting is one of the most practical skills in data science. In this short post, I’ll walk you through a classic example using Triple Exponential Smoothing (also known as the Holt-Winters method) on the famous Airline Passengers dataset.
Double exponential smoothing adds a trend component.
Triple exponential smoothing goes one step further — it also captures seasonality. This makes it especially useful for data that shows:
- A clear trend (upward or downward)
- Repeating seasonal patterns (e.g., yearly cycles)
- One for the level
- One for the trend
- One for the seasonal component
- A strong upward trend
- Clear yearly seasonality (more passengers in summer, fewer in winter)
- Increasing seasonal amplitude over time
- Split the data
- Training set: All data except the last 24 months
- Test set: The final 24 months (used to evaluate performance)
- Fit a Holt-Winters model with:
- Multiplicative trend
- Multiplicative seasonality
- Seasonal period = 12 (yearly pattern)
- Generate forecasts:
- Forecast the 24-month test period
- Extend the forecast another 5 years into the future
# ============================================================
# Triple Exponential Smoothing (Holt-Winters)
# Classic Airline Passengers + 5-Year Future Forecast
# ============================================================
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from sklearn.metrics import mean_absolute_error, mean_squared_error
# ------------------------------------------------------------
# 1. Load the classic Airline Passengers data
# ------------------------------------------------------------
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv"
data = pd.read_csv(url, parse_dates=['Month'], index_col='Month')
data.columns = ['Passengers']
data = data.asfreq('MS') # Monthly frequency
print("Data shape:", data.shape)
print(data.head())
print(data.tail())
# ------------------------------------------------------------
# 2. Train / Test Split
# ------------------------------------------------------------
train = data.iloc[:-24] # All data except last 24 months
test = data.iloc[-24:] # Last 24 months (2 years)
print(f"\nTrain size : {len(train)} months")
print(f"Test size : {len(test)} months")
# ------------------------------------------------------------
# 3. Build the Holt-Winters model
# ------------------------------------------------------------
"""
Parameters:
- trend='mul' → multiplicative trend
- seasonal='mul' → multiplicative seasonality
- seasonal_periods=12 → yearly seasonality (monthly data)
"""
model = ExponentialSmoothing(
train['Passengers'],
trend='mul',
seasonal='mul',
seasonal_periods=12,
damped_trend=False
)
# ------------------------------------------------------------
# 4. Fit the model
# ------------------------------------------------------------
fitted_model = model.fit(optimized=True)
print("\n===== Model Summary =====")
print(fitted_model.summary())
# ------------------------------------------------------------
# 5. Forecast
# ------------------------------------------------------------
# Forecast for the test period (24 months)
forecast_test = fitted_model.forecast(len(test))
# Forecast additional 5 years (60 months) into the future
future_steps = 60
forecast_future = fitted_model.forecast(len(test) + future_steps)
# Extract only the future 5-year part
forecast_5years = forecast_future[-future_steps:]
# Create datetime index for the 5-year forecast
last_date = test.index[-1]
future_index = pd.date_range(
start=last_date + pd.offsets.MonthBegin(1),
periods=future_steps,
freq='MS'
)
# ------------------------------------------------------------
# 6. Evaluation (on test set only)
# ------------------------------------------------------------
mae = mean_absolute_error(test['Passengers'], forecast_test)
rmse = np.sqrt(mean_squared_error(test['Passengers'], forecast_test))
mape = np.mean(np.abs((test['Passengers'] - forecast_test) / test['Passengers'])) * 100
print(f"\nTest MAE : {mae:.2f}")
print(f"Test RMSE : {rmse:.2f}")
print(f"Test MAPE : {mape:.2f}%")
# ------------------------------------------------------------
# 7. Main Plot (Train + Test + Forecast on Test + 5-Year Future)
# ------------------------------------------------------------
plt.figure(figsize=(15, 6))
plt.plot(train.index, train['Passengers'], label='Train', color='steelblue')
plt.plot(test.index, test['Passengers'], label='Test (Actual)', color='green')
plt.plot(test.index, forecast_test, label='Forecast (Test period)',
color='red', linestyle='--', linewidth=2)
plt.plot(future_index, forecast_5years, label='Forecast (Next 5 years)',
color='darkorange', linestyle='--', linewidth=2)
plt.title('Triple Exponential Smoothing (Holt-Winters)\nAirline Passengers + 5-Year Future Forecast')
plt.xlabel('Date')
plt.ylabel('Passengers (thousands)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ------------------------------------------------------------
# 8. In-sample Fit Plot
# ------------------------------------------------------------
plt.figure(figsize=(14, 5))
plt.plot(train.index, train['Passengers'], label='Actual (Train)')
plt.plot(train.index, fitted_model.fittedvalues, label='Fitted values', alpha=0.85)
plt.title('In-sample Fit of Holt-Winters')
plt.xlabel('Date')
plt.ylabel('Passengers (thousands)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()