Lesson 5 · Real-World Data Analytics
Python Data Analytics #05: Sales Forecasting for Retail Demand in Python
Video five of the hundred-video real-world data analytics series. Building a real weekly revenue forecast, tested honestly against real held-out weeks the…
- CourseReal-World Data Analytics
- Lesson5 of 26
- Video29 min
- FormatJupyter notebook · 30 code cells
- Data1 dataset
What you'll learn
Datasets used in this lesson
Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.
- online_retail_clean.csv36.2 MB
📓 Full notebook
Download .ipynbData Analytics 100, Video 5: Sales Forecasting for Retail Demand#
- Video five of the hundred-video real-world data analytics series.
- Building a real weekly revenue forecast, tested honestly against real held-out weeks the model never saw.
- Let's get into it.
Part 1: Why Forecasting Needs a Real Honest Test#
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
clean = pd.read_csv('online_retail_clean.csv', parse_dates=['InvoiceDate'])
clean.shape
Part 2: Real Daily Revenue Series#
clean['DateOnly'] = clean['InvoiceDate'].dt.date
daily = clean.groupby('DateOnly')['Revenue'].sum()
daily.index = pd.to_datetime(daily.index)
daily.shape[0]
Part 3: Filling Real Gaps with Zero-Revenue Days#
full_range = pd.date_range(daily.index.min(), daily.index.max(), freq='D')
daily = daily.reindex(full_range, fill_value=0)
daily.shape[0]
Part 4: Real Day-of-Week Pattern#
dow_avg = daily.groupby(daily.index.dayofweek).mean()
dow_avg.round(0)
Part 5: Confirming the Real Saturday Gap#
saturday_avg = dow_avg[5]
saturday_avg == 0
Part 6: Aggregating to Real Weekly Revenue#
weekly = daily.resample('W').sum()
weekly.shape[0]
Part 7: Trimming Real Partial Weeks#
weekly = weekly.iloc[1:-1]
weekly.shape[0]
weekly.head(3)
Part 8: Visualizing the Real Weekly Revenue History#
plt.figure(figsize=(11, 5))
plt.plot(weekly.index, weekly.values, marker='o', color='navy')
plt.xlabel('Real Week')
plt.ylabel('Real Weekly Revenue (GBP)')
plt.title('Real Weekly Retail Revenue History')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('weekly_revenue_history.png', dpi=120)
plt.close()
Part 9: The Real Train/Test Split#
train = weekly.iloc[:-8]
test = weekly.iloc[-8:]
len(train), len(test)
Part 10: Real Error Metric Helpers#
def mae(actual, forecast):
return np.mean(np.abs(np.asarray(actual) - np.asarray(forecast)))
def rmse(actual, forecast):
return np.sqrt(np.mean((np.asarray(actual) - np.asarray(forecast)) ** 2))
Part 11: Real Baseline 1, the Naive Forecast#
naive_forecast = pd.Series([train.iloc[-1]] * len(test), index=test.index)
naive_mae, naive_rmse = mae(test, naive_forecast), rmse(test, naive_forecast)
round(naive_mae, 0), round(naive_rmse, 0)
Part 12: Real Baseline 2, Moving Average#
ma_value = train.iloc[-4:].mean()
ma_forecast = pd.Series([ma_value] * len(test), index=test.index)
ma_mae, ma_rmse = mae(test, ma_forecast), rmse(test, ma_forecast)
round(ma_mae, 0), round(ma_rmse, 0)
Part 13: Real Model, Holt Exponential Smoothing with Trend#
from statsmodels.tsa.holtwinters import ExponentialSmoothing
hw_model = ExponentialSmoothing(train, trend='add', seasonal=None).fit()
hw_forecast = hw_model.forecast(len(test))
hw_forecast.round(0)
Part 14: Scoring the Real Holt-Winters Model#
hw_mae, hw_rmse = mae(test, hw_forecast), rmse(test, hw_forecast)
round(hw_mae, 0), round(hw_rmse, 0)
Part 15: Comparing All Three Real Models#
comparison = pd.DataFrame({
'Model': ['Naive', 'Moving Average', 'Holt-Winters'],
'MAE': [naive_mae, ma_mae, hw_mae],
'RMSE': [naive_rmse, ma_rmse, hw_rmse]
})
comparison.round(0)
Part 16: How Much Better Is the Real Winning Model#
best_model_mae = comparison['MAE'].min()
improvement_pct = round((naive_mae - best_model_mae) / naive_mae * 100, 1)
print(f'The best real model cuts forecast error by {improvement_pct}% versus the real naive baseline.')
Part 17: Visualizing Real Forecasts Against Real Actuals#
plt.figure(figsize=(11, 6))
plt.plot(train.index[-12:], train.values[-12:], label='Real Training History', color='gray')
plt.plot(test.index, test.values, label='Real Actual', color='black', marker='o', linewidth=2)
plt.plot(test.index, naive_forecast.values, label='Real Naive Forecast', linestyle='--')
plt.plot(test.index, hw_forecast.values, label='Real Holt-Winters Forecast', linestyle='--')
plt.xlabel('Real Week')
plt.ylabel('Real Weekly Revenue (GBP)')
plt.title('Real Forecasts vs Real Actual Held-Out Weeks')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('forecast_vs_actual.png', dpi=120)
plt.close()
Part 18: Refitting on All Real Available Data#
final_model = ExponentialSmoothing(weekly, trend='add', seasonal=None).fit()
future_forecast = final_model.forecast(6)
future_forecast.round(0)
Part 19: Visualizing the Real Future Forecast#
plt.figure(figsize=(11, 6))
plt.plot(weekly.index, weekly.values, label='Real Historical Revenue', color='navy')
plt.plot(future_forecast.index, future_forecast.values, label='Real Future Forecast', color='crimson', marker='o', linestyle='--')
plt.xlabel('Real Week')
plt.ylabel('Real Weekly Revenue (GBP)')
plt.title('Real Historical Revenue Plus Six-Week Forecast')
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('future_revenue_forecast.png', dpi=120)
plt.close()
Part 20: Real Residual Check#
residuals = train - hw_model.fittedvalues
residuals.mean().round(1), residuals.std().round(1)
Part 21: Saving the Real Forecast Results#
forecast_output = pd.DataFrame({'Week': future_forecast.index, 'ForecastedRevenue': future_forecast.values.round(2)})
forecast_output.to_csv('weekly_revenue_forecast.csv', index=False)
reloaded_forecast = pd.read_csv('weekly_revenue_forecast.csv')
len(reloaded_forecast) == len(future_forecast)
Part 22: Real Percentage Growth Implied by the Forecast#
last_actual_week = weekly.iloc[-1]
first_forecast_week = future_forecast.iloc[0]
growth_pct = round((first_forecast_week - last_actual_week) / last_actual_week * 100, 1)
print(f"The model expects next week's real revenue to move {growth_pct}% versus the most recent actual real week.")
Part 23: Real Rolling Backtest#
backtest_errors = []
for cutoff in [-20, -16, -12, -8]:
fold_train, fold_test = weekly.iloc[:cutoff], weekly.iloc[cutoff:cutoff+4]
fold_model = ExponentialSmoothing(fold_train, trend='add', seasonal=None).fit()
fold_forecast = fold_model.forecast(len(fold_test))
backtest_errors.append(mae(fold_test, fold_forecast))
[round(e, 0) for e in backtest_errors]
Part 24: Real Backtest Stability#
round(np.mean(backtest_errors), 0), round(np.std(backtest_errors), 0)
Part 25: Real Baseline 4, Simple Linear Trend#
x = np.arange(len(train))
slope, intercept = np.polyfit(x, train.values, 1)
future_x = np.arange(len(train), len(train) + len(test))
linear_forecast = pd.Series(slope * future_x + intercept, index=test.index)
linear_mae, linear_rmse = mae(test, linear_forecast), rmse(test, linear_forecast)
round(linear_mae, 0), round(linear_rmse, 0)
Part 26: Real Total Forecasted Revenue#
total_forecast_revenue = future_forecast.sum()
print(f'Total real forecasted revenue for the next six weeks: {round(total_forecast_revenue, 0):,.0f} GBP')
Part 27: Real Largest Single-Week Forecast Miss#
errors_by_week = (test - hw_forecast).abs()
worst_week = errors_by_week.idxmax()
print(f'Largest real miss: week of {worst_week.date()}, real actual {test[worst_week]:,.0f} vs real forecast {hw_forecast[worst_week]:,.0f}')
Part 28: Real Final Model Comparison Including Linear Trend#
full_comparison = pd.concat([comparison, pd.DataFrame({'Model': ['Linear Trend'], 'MAE': [linear_mae], 'RMSE': [linear_rmse]})], ignore_index=True)
full_comparison.sort_values('MAE').round(0)
full_comparison.to_csv('forecast_model_comparison.csv', index=False)
Part 29: Real Weekly Average Sanity Check#
manual_weekly_total = daily.loc[weekly.index[0] - pd.Timedelta(days=6):weekly.index[0]].sum()
abs(manual_weekly_total - weekly.iloc[0]) < 0.01
Part 30: Real Recap Print#
print(f'Forecasted {len(future_forecast)} real future weeks using Holt-Winters, beating the naive baseline by {improvement_pct}% on the real held-out test.')
Wrap-Up: What You Learned#
- Forecasting starts with one real evenly-spaced timeline; missing real days are genuinely zero, not gaps to drop.
- A real honest test hides real recent weeks and scores forecasts against them, never against data the model already saw.
- Naive and moving-average forecasts are real fast, genuinely useful floors that any smarter model has to actually beat.
- Holt's exponential smoothing tracks a real trend, letting the forecast keep moving instead of staying flat.
- A real production forecast gets refit on every real available week once the method has proven itself honestly.
- Next video: real price and discount impact analysis, looking at how real markdowns actually move real demand.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



