Lesson 23 · Supply Chain Operations Analytics
ARIMA and SARIMA for Demand Forecasting in Supply Chains
In this lesson, we explore how to use ARIMA and SARIMA models to forecast real-world retail demand. Accurate demand forecasting is crucial for inventory…
- CourseSupply Chain Operations Analytics
- Lesson23 of 27
- Video23 min
- FormatJupyter notebook · 19 code cells
What you'll learn
- Understanding Real-World Demand Data in Retail
- Beginner Example 1: Plotting and Inspecting Daily Demand
- Beginner Example 2: Checking for Stationarity
- Beginner Example 3: Differencing to Make Demand Stationary
- Intermediate Example 1: Fitting a Basic ARIMA Model
- Intermediate Example 2: Creating a Demand Forecast with ARIMA
- Intermediate Example 3: Model Diagnostics for ARIMA Residuals
- Advanced Example 1: Adding Seasonality with SARIMA
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbARIMA and SARIMA for Demand Forecasting in Supply Chains#
- In this lesson, we explore how to use ARIMA and SARIMA models to forecast real-world retail demand.
- Accurate demand forecasting is crucial for inventory planning, order scheduling, and reducing supply chain waste.
- You will learn how to prepare time series demand data, build ARIMA/SARIMA models, and interpret their outputs for operational actions.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.statespace.sarimax import SARIMAX
from statsmodels.tsa.arima.model import ARIMA
from pandas.plotting import register_matplotlib_converters
import warnings
warnings.filterwarnings('ignore')
register_matplotlib_converters()
Understanding Real-World Demand Data in Retail#
- Real supply chain data often includes dates, product codes, and order quantities.
- Data is usually transactional, not yet aggregated for time series forecasting.
- Beginners often forget to group over time periods or make mistakes with missing data.
- Clean time series input is essential for ARIMA and SARIMA to work properly.
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))
demand = df[['InvoiceDate', 'Quantity', 'StockCode']]
demand = demand[demand['Quantity'] > 0]
daily_demand = demand.groupby(demand['InvoiceDate'].dt.date)['Quantity'].sum()
daily_demand.index = pd.to_datetime(daily_demand.index)
daily_demand = daily_demand.asfreq('D', fill_value=0)
print(daily_demand.head(7))
Beginner Example 1: Plotting and Inspecting Daily Demand#
- First, always visualize your demand data to find anomalies or missing values.
- Outliers, unexpected zeros, or gaps can mean data entry or extraction errors.
plt.figure(figsize=(12,4))
daily_demand.plot(ax=plt.gca())
plt.title('Daily Retail Demand')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.show()
Beginner Example 2: Checking for Stationarity#
- ARIMA and SARIMA models assume stationary time series.
- Non-stationary demand can cause forecast errors or misleading signals.
from statsmodels.tsa.stattools import adfuller
adf_result = adfuller(daily_demand.values)
print('ADF Statistic: {:.4f}'.format(adf_result[0]))
print('p-value: {:.4f}'.format(adf_result[1]))
Beginner Example 3: Differencing to Make Demand Stationary#
- Differencing can help transform sales trends into stationary demand for ARIMA.
- Always review your differenced data before fitting a model.
diff_demand = daily_demand.diff().dropna()
plt.figure(figsize=(12,3))
diff_demand.plot(ax=plt.gca())
plt.title('Differenced Daily Demand')
plt.xlabel('Date')
plt.ylabel('Day-over-day Change')
plt.show()
Intermediate Example 1: Fitting a Basic ARIMA Model#
- ARIMA(p,d,q) models are popular for forecasting demand where p = AR lags, d = differencing, q = MA lags.
- We start with simple parameters and adjust based on diagnostics.
arima_model = ARIMA(daily_demand, order=(1,1,1))
arima_fit = arima_model.fit()
print(arima_fit.summary())
Intermediate Example 2: Creating a Demand Forecast with ARIMA#
- The real value of ARIMA is to generate future forecasts for retail operations.
- We create a short-term demand forecast and visualize the results.
forecast = arima_fit.get_forecast(steps=30)
predicted_mean = forecast.predicted_mean
conf_int = forecast.conf_int()
plt.figure(figsize=(12,4))
daily_demand.plot(label='Observed')
predicted_mean.plot(label='Forecast', color='r')
plt.fill_between(predicted_mean.index, conf_int.iloc[:,0], conf_int.iloc[:,1], color='pink', alpha=0.3)
plt.title('ARIMA 30-Day Demand Forecast')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.legend()
plt.show()
Intermediate Example 3: Model Diagnostics for ARIMA Residuals#
- After fitting an ARIMA, always check if the residuals are random (white noise).
- Residuals should not show autocorrelation or obvious patterns over time.
residuals = arima_fit.resid
plt.figure(figsize=(12,4))
plt.plot(residuals)
plt.title('ARIMA Residuals (Should Look Random)')
plt.xlabel('Date')
plt.ylabel('Error')
plt.show()
from statsmodels.graphics.tsaplots import plot_acf
plot_acf(residuals.dropna(), lags=30)
plt.title('ACF of ARIMA Residuals')
plt.show()
Advanced Example 1: Adding Seasonality with SARIMA#
- Retail and supply chain demand often shows weekly or yearly seasonality.
- SARIMA includes explicit seasonal (P, D, Q, S) terms for better long-run forecasts.
sarima_model = SARIMAX(daily_demand, order=(1,1,1), seasonal_order=(1,1,1,7), enforce_stationarity=False, enforce_invertibility=False)
sarima_fit = sarima_model.fit(disp=False)
print(sarima_fit.summary())
sarima_forecast = sarima_fit.get_forecast(steps=30)
sarima_pred_mean = sarima_forecast.predicted_mean
sarima_conf_int = sarima_forecast.conf_int()
plt.figure(figsize=(12,4))
daily_demand.plot(label='Observed')
sarima_pred_mean.plot(label='SARIMA Forecast', color='green')
plt.fill_between(sarima_pred_mean.index, sarima_conf_int.iloc[:,0], sarima_conf_int.iloc[:,1], color='lightgreen', alpha=0.3)
plt.title('SARIMA 30-Day Demand Forecast (Weekly Seasonality)')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.legend()
plt.show()
Advanced Example 2: Fitting and Forecasting Demand for a Single Product#
- Often, we want to forecast demand not just for the whole store, but for a single StockCode (SKU/item).
- This helps with SKU-level replenishment, automated ordering, and targeted promotions.
example_sku = demand['StockCode'].value_counts().idxmax()
sku_demand = demand[demand['StockCode'] == example_sku]
sku_daily = sku_demand.groupby(sku_demand['InvoiceDate'].dt.date)['Quantity'].sum()
sku_daily.index = pd.to_datetime(sku_daily.index)
sku_daily = sku_daily.asfreq('D', fill_value=0)
sku_model = SARIMAX(sku_daily, order=(1,1,1), seasonal_order=(1,1,1,7), enforce_stationarity=False, enforce_invertibility=False)
sku_fit = sku_model.fit(disp=False)
sku_forecast = sku_fit.get_forecast(steps=14)
sku_pred_mean = sku_forecast.predicted_mean
plt.figure(figsize=(10,4))
sku_daily.plot(label='SKU Demand')
sku_pred_mean.plot(label='Forecast', color='orange')
plt.title(f'14-Day Forecast for SKU {example_sku}')
plt.xlabel('Date')
plt.ylabel('SKU Demand')
plt.legend()
plt.show()
Error Handling Example 1: Detecting and Filling Missing Dates#
- Missing time periods can break ARIMA/SARIMA or distort supply forecasts.
- Fill gaps so every period is accounted for in demand modeling.
all_days = pd.date_range(daily_demand.index.min(), daily_demand.index.max(), freq='D')
missing_days = all_days.difference(daily_demand.index)
print(f'Missing days in original demand series: {len(missing_days)}')
if len(missing_days) > 0:
print(missing_days)
if len(missing_days) > 0:
fixed_demand = daily_demand.reindex(all_days, fill_value=0)
else:
fixed_demand = daily_demand
print(f'Fixed daily demand shape: {fixed_demand.shape}')
Error Handling Example 2: Common Join Mistake with Product Demand#
- Merging wrong keys or forgetting to use both date and product code causes data errors.
- Always use both date and StockCode in groupby/merge for multiproduct demand forecasting.
# Incorrect aggregation: grouping by only date, ignoring product
multi_demand = demand.groupby(demand['InvoiceDate'].dt.date)['Quantity'].sum()
# Correct way: group by both date and StockCode
correct_multi = demand.groupby([demand['InvoiceDate'].dt.date, 'StockCode'])['Quantity'].sum().unstack(fill_value=0)
print('Incorrect grouped shape:', multi_demand.shape)
print('Correct multiproduct grouped shape:', correct_multi.shape)
Best Practice: Aggregating Weekly Demand for Operational KPIs#
- Many supply chain KPIs are tracked weekly, not daily.
- Use weekly aggregation for smoother, actionable metrics.
weekly_demand = daily_demand.resample('W').sum()
plt.figure(figsize=(10,3))
weekly_demand.plot(ax=plt.gca(), color='navy')
plt.title('Weekly Total Retail Demand')
plt.xlabel('Week')
plt.ylabel('Quantity Sold')
plt.show()
Pattern: Calculating a Rolling Service Level#
- Rolling KPIs like fill rate or service level show how often stock meets demand.
- Use moving averages to smooth KPI charts for operations.
rolling_service = (daily_demand > 0).rolling(14).mean()
plt.figure(figsize=(10,3))
rolling_service.plot()
plt.title('Rolling 14-Day Service Level (Nonzero Days)')
plt.xlabel('Date')
plt.ylabel('Service Level')
plt.ylim(0,1.1)
plt.show()
End-to-End Example: From Raw Retail Data to Demand Action#
- We will aggregate, clean, and forecast demand, producing a CSV with predicted next-month demand.
- This output can drive reorder quantities and automated system triggers.
final_model = SARIMAX(daily_demand, order=(1,1,1), seasonal_order=(1,1,1,7), enforce_stationarity=False, enforce_invertibility=False)
final_fit = final_model.fit(disp=False)
monthly_forecast = final_fit.get_forecast(steps=31).predicted_mean
monthly_forecast = monthly_forecast.rename('predicted_quantity')
monthly_forecast.to_csv('next_month_demand_forecast.csv', index_label='date')
print('Next-month demand forecast saved to next_month_demand_forecast.csv')
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



