Mathew K Analytics

Lesson 22 · Supply Chain Operations Analytics

Moving Average and Exponential Smoothing Models in Supply Chain Analytics

Learn how to forecast product demand using moving average and exponential smoothing. These models help businesses predict future demand when recent trends…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Moving Average and Exponential Smoothing Models in Supply Chain Analytics#

  • Learn how to forecast product demand using moving average and exponential smoothing.
  • These models help businesses predict future demand when recent trends and seasonality matter.
  • We will use real retail sales data to build, visualize, and evaluate these models.
  • By the end, you will know how to improve inventory planning and reduce operational risks.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

Understanding our operational dataset#

  • We will use historical retail demand data from a real UK online retailer.
  • Each row is a transaction, with date, product ID, and quantity sold.
  • It is common to trip up on date parsing or aggregating multiple sales correctly.
  • Make sure you group data by both product and date for time series analysis.
# Load real-world retail demand data
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df = pd.read_excel(url, sheet_name='Year 2010-2011')
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
print(df.shape)
print(df.head(3))
(541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  
# Quick data cleaning: remove cancellations and errors
df = df[df['Quantity'] > 0]
df = df[df['Invoice'].astype(str).str.startswith('C') == False]
print(df.shape)
(531286, 8)
# Select a single product for focused analysis
item_code = df['StockCode'].value_counts().idxmax()
item_df = df[df['StockCode'] == item_code]
print(f'Analyzing product {item_code}')
Analyzing product 85123A
# Aggregate daily demand for the selected product
daily_sales = (item_df.groupby(item_df['InvoiceDate'].dt.date)['Quantity']
              .sum()
              .rename('sales'))
daily_sales = daily_sales.asfreq('D', fill_value=0)
print(daily_sales.head())
InvoiceDate
2010-12-01    454
2010-12-02    309
2010-12-03     25
2010-12-04      0
2010-12-05    198
Freq: D, Name: sales, dtype: int64
# Visualize daily demand
plt.figure(figsize=(12,4))
plt.plot(daily_sales, marker='o', linestyle='-')
plt.title('Daily Demand for Product ' + str(item_code))
plt.xlabel('Date')
plt.ylabel('Units Sold')
plt.show()
No description has been provided for this image

Beginner Example 1: 7-Day Simple Moving Average#

  • The simple moving average forecasts demand by averaging the last N days.
  • In supply chains, this method smooths out random fluctuations.
  • A 7-day average helps plan weekly replenishment cycles.
# Calculate and plot 7-day moving average
ma7 = daily_sales.rolling(window=7).mean()
plt.figure(figsize=(12,4))
plt.plot(daily_sales.index, daily_sales.values, label='Actual', alpha=0.5)
plt.plot(ma7.index, ma7.values, label='7-Day MA', linewidth=2)
plt.legend()
plt.title('7-Day Simple Moving Average Forecast')
plt.show()
No description has been provided for this image

Beginner Example 2: 14-Day Moving Average#

  • Extending the window to 14 days further smooths out short-term noise.
  • Longer windows are useful when seasonal patterns occur over biweekly cycles.
# 14-day simple moving average forecast
ma14 = daily_sales.rolling(window=14).mean()
plt.figure(figsize=(12,4))
plt.plot(daily_sales.index, daily_sales.values, label='Actual', alpha=0.5)
plt.plot(ma14.index, ma14.values, label='14-Day MA', linewidth=2, color='orange')
plt.legend()
plt.title('14-Day Simple Moving Average Forecast')
plt.show()
No description has been provided for this image

Beginner Example 3: Moving Average as Next-Day Forecast#

  • In practice, tomorrow's sales are unknown: we must use only past data to predict.
  • We can shift the moving average result forward by one day to make a true operational forecast.
# Create a true next-day moving average prediction
forecast = ma7.shift(1)
plt.figure(figsize=(12,4))
plt.plot(daily_sales.index, daily_sales.values, label='Actual', alpha=0.5)
plt.plot(forecast.index, forecast.values, label='Next-Day Moving Avg Forecast', linestyle='--')
plt.legend()
plt.title('Operational 7-Day Moving Average Forecast (Shifted)')
plt.show()
No description has been provided for this image

Intermediate Example 1: Evaluating Forecast Accuracy#

  • Supply chain managers need to know how accurate their forecasts are.
  • Let us calculate the Mean Absolute Error (MAE) for our shifted moving average forecast.
# Calculate MAE for 7-day moving average forecast
mae = np.mean(np.abs(daily_sales[7:] - forecast[7:]))
print(f'Mean Absolute Error: {mae:.2f} units per day')
Mean Absolute Error: 120.07 units per day

Intermediate Example 2: Simple Exponential Smoothing Forecast#

  • Exponential Smoothing weighs recent sales more heavily than older data.
  • This method is effective when demand levels shift but there is little seasonality.
from statsmodels.tsa.holtwinters import SimpleExpSmoothing
# Fit simple exponential smoothing with alpha=0.3
model = SimpleExpSmoothing(daily_sales).fit(smoothing_level=0.3, optimized=False)
exp_forecast = model.fittedvalues.shift(1)
plt.figure(figsize=(12,4))
plt.plot(daily_sales.index, daily_sales.values, label='Actual', alpha=0.5)
plt.plot(exp_forecast.index, exp_forecast.values, label='Exp Smoothing', linestyle='--', color='g')
plt.legend()
plt.title('Simple Exponential Smoothing Forecast (alpha=0.3)')
plt.show()
No description has been provided for this image
# Compute MAE for exponential smoothing
exp_mae = np.mean(np.abs(daily_sales[1:] - exp_forecast[1:]))
print(f'Exponential Smoothing MAE: {exp_mae:.2f} units per day')
Exponential Smoothing MAE: 123.81 units per day

Intermediate Example 3: Parameter Tuning for Exponential Smoothing#

  • Tuning alpha changes the model's sensitivity to recent changes.
  • Supply chain planners must choose alpha based on how stable or volatile demand is.
# Try different alpha values and compare MAE
maes = []
alphas = np.arange(0.1, 1.0, 0.1)
for alpha in alphas:
    model = SimpleExpSmoothing(daily_sales).fit(smoothing_level=alpha, optimized=False)
    forecast = model.fittedvalues.shift(1)
    mae = np.mean(np.abs(daily_sales[1:] - forecast[1:]))
    maes.append(mae)
plt.figure(figsize=(8,4))
plt.plot(alphas, maes, marker='o')
plt.xlabel('Alpha (Smoothing Level)')
plt.ylabel('MAE')
plt.title('Forecast Error vs. Alpha Parameter')
plt.show()
No description has been provided for this image

Advanced Example 1: Operationalizing Forecasts Weekly Ordering#

  • Supply teams order stock in batches, often weekly.
  • Aggregate your daily forecast to generate weekly order recommendations.
# Aggregate forecast for weekly replenishment
weekly_fcst = forecast.resample('W').sum()
weekly_actual = daily_sales.resample('W').sum()
plt.figure(figsize=(12,4))
plt.plot(weekly_actual.index, weekly_actual.values, label='Actual Weekly Sales')
plt.plot(weekly_fcst.index, weekly_fcst.values, label='Forecasted Weekly Sales')
plt.title('Weekly Demand vs Weekly Forecast (7-Day MA)')
plt.xlabel('Week')
plt.ylabel('Units')
plt.legend()
plt.show()
No description has been provided for this image

Advanced Example 2: Exponential Smoothing with Automated Alpha Selection#

  • Manually tuning alpha is time-consuming in practice.
  • Let us fit exponential smoothing with the optimized parameter.
  • This is commonly done in business forecasting tools.
# Fit exponential smoothing and let library tune alpha
auto_model = SimpleExpSmoothing(daily_sales).fit(optimized=True)
auto_fcst = auto_model.fittedvalues.shift(1)
auto_alpha = auto_model.model.params['smoothing_level']
auto_mae = np.mean(np.abs(daily_sales[1:] - auto_fcst[1:]))
print(f'Best-fit alpha: {auto_alpha:.2f}, MAE: {auto_mae:.2f}')
Best-fit alpha: 0.05, MAE: 120.26
# Visualize optimal forecast vs. actual demand
plt.figure(figsize=(12,4))
plt.plot(daily_sales.index, daily_sales.values, label='Actual', alpha=0.5)
plt.plot(auto_fcst.index, auto_fcst.values, label='Optimized Exp Smoothing', linestyle='--', color='r')
plt.legend()
plt.title(f'Best Alpha ({auto_alpha:.2f})  Automated Exponential Smoothing Forecast')
plt.show()
No description has been provided for this image

Error Handling Example 1: Checking for Missing Dates#

  • In real retail data, missing dates can break rolling forecasts.
  • Let us identify any missing dates in our processed sales time series.
# Identify missing dates in the demand series
all_days = pd.date_range(daily_sales.index.min(), daily_sales.index.max(), freq='D')
missing_days = all_days.difference(pd.to_datetime(daily_sales.index))
print(f'Missing days: {missing_days}')
Missing days: DatetimeIndex([], dtype='datetime64[ns]', freq='D')

Error Handling Example 2: Handling Out-of-Order or Duplicated Entries#

  • Sometimes time series are not sorted, or have duplicate entries.
  • Let us make sure our series is strictly increasing and aggregate duplicates.
# Sort and aggregate sales by date in case of issues
daily_sales_clean = daily_sales.groupby(daily_sales.index).sum().sort_index()
print(daily_sales_clean.head())
InvoiceDate
2010-12-01    454
2010-12-02    309
2010-12-03     25
2010-12-04      0
2010-12-05    198
Freq: D, Name: sales, dtype: int64

Error Handling Example 3: Misinterpreting Lead Times#

  • If lead times are not considered, forecasts may suggest orders that arrive too late.
  • Always align your forecast time horizon with operational lead times.
# Simulate an operational lead time of 3 days
lead_time = 3
future_forecast = ma7.shift(lead_time)
print('Lead-time adjusted forecast:')
print(future_forecast.head(10))
Lead-time adjusted forecast:
InvoiceDate
2010-12-01           NaN
2010-12-02           NaN
2010-12-03           NaN
2010-12-04           NaN
2010-12-05           NaN
2010-12-06           NaN
2010-12-07           NaN
2010-12-08           NaN
2010-12-09           NaN
2010-12-10    211.142857
Freq: D, Name: sales, dtype: float64

Best Practice: Resample and Aggregate for KPI Reporting#

  • Always aggregate operational data to the correct time unit for the business question.
  • Let us create a monthly report of total sales and use it for KPI calculation.
# Aggregate sales by month for KPI reporting
monthly_sales = daily_sales.resample('M').sum()
print(monthly_sales.head())
InvoiceDate
2010-12-31    3753
2011-01-31    5533
2011-02-28    1877
2011-03-31    1999
2011-04-30    3843
Freq: ME, Name: sales, dtype: int64
# Calculate a monthly growth rate KPI
monthly_growth = monthly_sales.pct_change().fillna(0)
print('Monthly demand growth rates (%):')
print((monthly_growth * 100).round(2))
Monthly demand growth rates (%):
InvoiceDate
2010-12-31      0.00
2011-01-31     47.43
2011-02-28    -66.08
2011-03-31      6.50
2011-04-30     92.25
2011-05-31      4.24
2011-06-30     41.69
2011-07-31    -46.99
2011-08-31    -31.01
2011-09-30     19.36
2011-10-31    -31.84
2011-11-30    190.70
2011-12-31    -83.40
Freq: ME, Name: sales, dtype: float64

Best Practice: Align Data for Time Series Models#

  • The order and spacing of your sales data must be correct for moving average and smoothing models.
  • Confirm no missing time steps, and handle special events (such as closed days) explicitly.
# Practice: Insert fake missing holiday and fill
holiday = pd.to_datetime('2010-12-25')
if holiday not in daily_sales.index:
    daily_sales.loc[holiday] = 0
daily_sales = daily_sales.sort_index().asfreq('D', fill_value=0)
print(daily_sales.loc['2010-12-24':'2010-12-26'])
InvoiceDate
2010-12-24    0
2010-12-25    0
2010-12-26    0
Freq: D, Name: sales, dtype: int64

End-to-End Supply Chain Example: Forecast, Order, and Report#

  • Let us put it all together: forecast, create a batch order plan, and produce a monthly KPI report.
  • We will simulate a demand forecast, translate to operational orders, then compute service level.
# Generate a forecast and compare to actual weekly sales
fcst = SimpleExpSmoothing(daily_sales).fit(optimized=True).fittedvalues.shift(1)
orders = fcst.resample('W').sum().round().astype(int)
actuals = daily_sales.resample('W').sum().astype(int)
order_df = pd.DataFrame({'OrderQTY': orders, 'ActualQTY': actuals})
order_df['MetDemand'] = np.minimum(order_df['OrderQTY'], order_df['ActualQTY'])
order_df['ServiceLevel'] = order_df['MetDemand'] / order_df['ActualQTY'].replace(0, np.nan)
print(order_df.head())
             OrderQTY  ActualQTY  MetDemand  ServiceLevel
InvoiceDate                                              
2010-12-05       1780        986        986           1.0
2010-12-12       2629       1045       1045           1.0
2010-12-19       2210       1524       1524           1.0
2010-12-26       1824        198        198           1.0
2011-01-02       1291          0          0           NaN
# Create a KPI report of average service level by month
order_df['Month'] = order_df.index.to_period('M')
monthly_service = order_df.groupby('Month')['ServiceLevel'].mean().round(3)
print('Average monthly service level:')
print(monthly_service)
Average monthly service level:
Month
2010-12    1.000
2011-01    0.862
2011-02    1.000
2011-03    1.000
2011-04    0.795
2011-05    0.914
2011-06    0.817
2011-07    0.923
2011-08    0.941
2011-09    0.900
2011-10    0.914
2011-11    0.747
2011-12    0.973
Freq: M, Name: ServiceLevel, dtype: float64
 

Found this useful?

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