Mathew K Analytics

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…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

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 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))
(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  
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))
InvoiceDate
2010-12-01    27007
2010-12-02    31348
2010-12-03    16471
2010-12-04        0
2010-12-05    16451
2010-12-06    21951
2010-12-07    25365
Freq: D, Name: Quantity, dtype: int64

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()
No description has been provided for this image

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]))
ADF Statistic: -0.8731
p-value: 0.7967

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()
No description has been provided for this image

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())
                               SARIMAX Results                                
==============================================================================
Dep. Variable:               Quantity   No. Observations:                  374
Model:                 ARIMA(1, 1, 1)   Log Likelihood               -3994.707
Date:                Sun, 25 Jan 2026   AIC                           7995.414
Time:                        17:21:30   BIC                           8007.178
Sample:                    12-01-2010   HQIC                          8000.085
                         - 12-09-2011                                         
Covariance Type:                  opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ar.L1          0.2993      0.077      3.898      0.000       0.149       0.450
ma.L1         -0.9629      0.021    -46.374      0.000      -1.004      -0.922
sigma2      1.393e+08    2.1e-10   6.62e+17      0.000    1.39e+08    1.39e+08
===================================================================================
Ljung-Box (L1) (Q):                   0.81   Jarque-Bera (JB):              1090.29
Prob(Q):                              0.37   Prob(JB):                         0.00
Heteroskedasticity (H):               1.73   Skew:                             1.28
Prob(H) (two-sided):                  0.00   Kurtosis:                        10.97
===================================================================================

Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
[2] Covariance matrix is singular or near-singular, with condition number 2.13e+33. Standard errors may be unstable.

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()
No description has been provided for this image

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()
No description has been provided for this image
No description has been provided for this image

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())
                                     SARIMAX Results                                     
=========================================================================================
Dep. Variable:                          Quantity   No. Observations:                  374
Model:             SARIMAX(1, 1, 1)x(1, 1, 1, 7)   Log Likelihood               -3742.909
Date:                           Sun, 25 Jan 2026   AIC                           7495.817
Time:                                   17:26:01   BIC                           7515.206
Sample:                               12-01-2010   HQIC                          7503.529
                                    - 12-09-2011                                         
Covariance Type:                             opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ar.L1          0.2047      0.099      2.069      0.039       0.011       0.399
ma.L1         -0.9634      0.048    -19.947      0.000      -1.058      -0.869
ar.S.L7        0.1552      0.105      1.481      0.139      -0.050       0.360
ma.S.L7       -0.8329      0.070    -11.891      0.000      -0.970      -0.696
sigma2      1.031e+08   1.01e-09   1.02e+17      0.000    1.03e+08    1.03e+08
===================================================================================
Ljung-Box (L1) (Q):                   0.00   Jarque-Bera (JB):              6250.99
Prob(Q):                              0.99   Prob(JB):                         0.00
Heteroskedasticity (H):               1.06   Skew:                             2.74
Prob(H) (two-sided):                  0.74   Kurtosis:                        22.75
===================================================================================

Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
[2] Covariance matrix is singular or near-singular, with condition number 2.02e+32. Standard errors may be unstable.
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()
No description has been provided for this image

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()
No description has been provided for this image

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)
Missing days in original demand series: 0
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}')
Fixed daily demand shape: (374,)

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)
Incorrect grouped shape: (305,)
Correct multiproduct grouped shape: (305, 3941)

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()
No description has been provided for this image

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()
No description has been provided for this image

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')
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.