Mathew K Analytics

Lesson 8 · Supply Chain Operations Analytics

Daily and Monthly Demand Aggregations in Supply Chain Analytics

You will learn to analyze retail demand using real world datasets. Demand aggregation helps supply chains optimize inventory and operations. By the end, you…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Daily and Monthly Demand Aggregations in Supply Chain Analytics#

  • You will learn to analyze retail demand using real world datasets.
  • Demand aggregation helps supply chains optimize inventory and operations.
  • By the end, you can compute and visualize daily and monthly demand for real sales data.
  • You will practice techniques used for forecasting, capacity planning, and supply chain monitoring.
  • Daily and monthly insights allow better management of inventory and smoother operations.
  • Let us dive in and solve a real operational analytics challenge.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

What is Demand Data and Why is it Important?#

  • Demand data shows which products are bought, when, and by whom.
  • It drives procurement, inventory, production planning, and sales forecasting.
  • In retail, each sales transaction becomes a demand signal.
  • Datasets usually have Date, Product ID, Quantity, and sometimes Store/Customer columns.
  • Beginners often miss issues like non-uniform time intervals or missing demand days.
  • Analyzing aggregate demand helps supply chains avoid stockouts and excess inventory.
# Load retail demand data from UCI repository
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  
# Look for missing values in key columns
print(df[['InvoiceDate','Quantity', 'StockCode']].isnull().sum())
InvoiceDate    0
Quantity       0
StockCode      0
dtype: int64
# Filter the data to one country (United Kingdom) for consistency
country_df = df[df['Country'] == 'United Kingdom'].copy()
print(country_df.shape)
(495478, 8)
# Extract date only for daily aggregation
country_df['InvoiceDay'] = country_df['InvoiceDate'].dt.date
print(country_df[['InvoiceDate','InvoiceDay']].head(3))
          InvoiceDate  InvoiceDay
0 2010-12-01 08:26:00  2010-12-01
1 2010-12-01 08:26:00  2010-12-01
2 2010-12-01 08:26:00  2010-12-01
# Beginner example 1: Aggregate total demand per day across all products
daily_demand = country_df.groupby('InvoiceDay')['Quantity'].sum().reset_index()
print(daily_demand.head())
   InvoiceDay  Quantity
0  2010-12-01     23949
1  2010-12-02     20873
2  2010-12-03     10439
3  2010-12-05     13604
4  2010-12-06     20669
# Beginner example 2: Plot daily demand over time
plt.figure(figsize=(12,4))
plt.plot(daily_demand['InvoiceDay'], daily_demand['Quantity'])
plt.title('Total Daily Demand - UK Retailer')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.tight_layout()
plt.show()
No description has been provided for this image
# Beginner example 3: What is the average demand per day?
avg_demand = daily_demand['Quantity'].mean()
print(f'Average daily demand: {avg_demand:.2f} units')
Average daily demand: 13979.77 units
# Intermediate example 1: Aggregate demand by both date and product
daily_product_demand = country_df.groupby(['InvoiceDay','StockCode'])['Quantity'].sum().reset_index()
print(daily_product_demand.head())
   InvoiceDay StockCode  Quantity
0  2010-12-01     10002        12
1  2010-12-01     10125         2
2  2010-12-01     10133         5
3  2010-12-01     10135         1
4  2010-12-01     11001         3
# Intermediate example 2: Find top 5 selling products overall
top_products = country_df.groupby('StockCode')['Quantity'].sum().sort_values(ascending=False).head(5)
print(top_products)
StockCode
22197     52928
84077     48326
85099B    43167
85123A    36706
84879     33519
Name: Quantity, dtype: int64
# Intermediate example 3: Aggregate and plot monthly demand
country_df['InvoiceMonth'] = country_df['InvoiceDate'].dt.to_period('M').dt.to_timestamp()
monthly_demand = country_df.groupby('InvoiceMonth')['Quantity'].sum().reset_index()
plt.figure(figsize=(10,5))
plt.bar(monthly_demand['InvoiceMonth'].dt.strftime('%Y-%m'), monthly_demand['Quantity'])
plt.title('Total Monthly Demand - UK Retailer')
plt.xlabel('Month')
plt.ylabel('Quantity Sold')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
No description has been provided for this image
# Intermediate example 4: Find days with missing demand (zero-sales days)
all_days = pd.date_range(country_df['InvoiceDay'].min(), country_df['InvoiceDay'].max(), freq='D')
existing_days = set(daily_demand['InvoiceDay'])
missing_days = [day for day in all_days.date if day not in existing_days]
print(f'Missing demand days: {len(missing_days)}')
print(missing_days[:5])
Missing demand days: 69
[datetime.date(2010, 12, 4), datetime.date(2010, 12, 11), datetime.date(2010, 12, 18), datetime.date(2010, 12, 24), datetime.date(2010, 12, 25)]
# Intermediate example 5: Aggregate demand by customer type (wholesale vs retail)
if 'InvoiceNo' in country_df and country_df['InvoiceNo'].dtype == object:
    country_df['InvoiceType'] = country_df['InvoiceNo'].str.startswith('C').map(lambda x: 'Credit' if x else 'Normal')
    invtype_summary = country_df.groupby('InvoiceType')['Quantity'].sum()
    print(invtype_summary)
# Advanced example 1: Create a pivot table of daily demand by top 3 products
top3_codes = top_products.index[:3]
pivot_df = daily_product_demand[daily_product_demand['StockCode'].isin(top3_codes)]
pivot = pivot_df.pivot(index='InvoiceDay', columns='StockCode', values='Quantity').fillna(0)
print(pivot.head())
StockCode   22197   84077  85099B
InvoiceDay                       
2010-12-01  199.0     0.0   556.0
2010-12-02   72.0  3264.0    48.0
2010-12-03   67.0    49.0     9.0
2010-12-05   57.0    96.0    29.0
2010-12-06  264.0     8.0   157.0
# Advanced example 2: Compute rolling average demand per product (7-day window)
pivot_rolling = pivot.rolling(7, min_periods=1).mean()
print(pivot_rolling.head(10))
StockCode        22197        84077      85099B
InvoiceDay                                     
2010-12-01  199.000000     0.000000  556.000000
2010-12-02  135.500000  1632.000000  302.000000
2010-12-03  112.666667  1104.333333  204.333333
2010-12-05   98.750000   852.250000  160.500000
2010-12-06  131.800000   683.400000  159.800000
2010-12-07  131.333333   577.833333  190.166667
2010-12-08  132.571429   529.571429  192.285714
2010-12-09  134.428571   537.000000  115.428571
2010-12-10  137.714286    80.285714  123.285714
2010-12-12  135.142857    87.000000  122.142857
# Advanced example 3: Aggregate demand by weekday to spot operational peaks
country_df['Weekday'] = country_df['InvoiceDate'].dt.day_name()
weekday_demand = country_df.groupby('Weekday')['Quantity'].sum().reindex([
    'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
])
print(weekday_demand)
Weekday
Monday       679860.0
Tuesday      796467.0
Wednesday    792094.0
Thursday     936414.0
Friday       646879.0
Saturday          NaN
Sunday       412115.0
Name: Quantity, dtype: float64
# Error handling 1: What if there are missing dates in your aggregation?
try:
    # Check for missing days and fill
    full_daily = daily_demand.set_index('InvoiceDay').reindex(all_days.date).fillna(0)
    print(full_daily.head())
except Exception as e:
    print(f'Error encountered: {e}')
            Quantity
InvoiceDay          
2010-12-01   23949.0
2010-12-02   20873.0
2010-12-03   10439.0
2010-12-04       0.0
2010-12-05   13604.0
# Error handling 2: What if quantities are negative (returns)?
neg_qty = country_df[country_df['Quantity'] < 0]
print(f'Number of negative demand rows: {neg_qty.shape[0]}')
print(neg_qty[['InvoiceDate','StockCode','Quantity']].head())
Number of negative demand rows: 9192
            InvoiceDate StockCode  Quantity
141 2010-12-01 09:41:00         D        -1
154 2010-12-01 09:49:00    35004C        -1
235 2010-12-01 10:24:00     22556       -12
236 2010-12-01 10:24:00     21984       -24
237 2010-12-01 10:24:00     21983       -24
# Error handling 3: What if two aggregations produce mismatched results?
daily_sum = country_df.groupby('InvoiceDay')['Quantity'].sum()
daily_mean = country_df.groupby('InvoiceDay')['Quantity'].mean()
if (daily_mean.index != daily_sum.index).any():
    print('Date mismatch between aggregations!')
else:
    print('Aggregation dates match.')
Aggregation dates match.

Best Practices for Demand Aggregation#

  • Always check for missing, duplicated, and zero-demand days.
  • Use explicit time periods (day, week, month) for all KPIs.
  • Separate product-level and company-level aggregations.
  • Rolling averages help smooth operational noise.
  • Pivot tables make comparison across products easier.
  • Visualize trends before reporting to your team.
  • Never join datasets without verifying matching time indices.
# Pattern: Get monthly, weekly, and daily demand in a single dataframe
kpi_df = country_df[['InvoiceDate','Quantity']].copy()
kpi_df['Day'] = kpi_df['InvoiceDate'].dt.date
kpi_df['Week'] = kpi_df['InvoiceDate'].dt.to_period('W').dt.start_time
kpi_df['Month'] = kpi_df['InvoiceDate'].dt.to_period('M').dt.to_timestamp()
agg_day = kpi_df.groupby('Day')['Quantity'].sum().rename('day_qty')
agg_week = kpi_df.groupby('Week')['Quantity'].sum().rename('week_qty')
agg_month = kpi_df.groupby('Month')['Quantity'].sum().rename('month_qty')
print(agg_month.head())
Month
2010-12-01    298101
2011-01-01    237381
2011-02-01    225641
2011-03-01    279843
2011-04-01    257666
Name: month_qty, dtype: int64

End-to-End Example: From Raw Transactions to Actionable Insights#

  • You will walk through loading, cleaning, aggregating and plotting real retail demand data.
  • You will find operational surges and slowdowns.
  • You will finish with a suggestion that can impact inventory or promotion strategy.
# Reload and clean data for a repeatable process
df = pd.read_excel(url, sheet_name='Year 2010-2011')
df = df[df['Country'] == 'United Kingdom']
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
df = df[df['Quantity'] > 0]
df['InvoiceDay'] = df['InvoiceDate'].dt.date
print(f'Post-cleaning rows: {df.shape[0]}')
Post-cleaning rows: 486286
# Aggregate and plot monthly demand after cleaning
monthly_demand = df.groupby(df['InvoiceDate'].dt.to_period('M').dt.to_timestamp())['Quantity'].sum()
monthly_demand.plot(kind='bar', figsize=(10,5))
plt.title('Monthly Demand After Cleaning')
plt.ylabel('Units Sold')
plt.xlabel('Month')
plt.tight_layout()
plt.show()
No description has been provided for this image
 

Found this useful?

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