Mathew K Analytics

Lesson 25 · Supply Chain Operations Analytics

Understanding Inventory Dynamics in Supply Chain Analytics

In this lesson, we will learn how to analyze and understand inventory behaviors using real-world supply chain datasets. Inventory management is crucial for…

📓 Full notebook

Download .ipynb

Understanding Inventory Dynamics in Supply Chain Analytics#

  • In this lesson, we will learn how to analyze and understand inventory behaviors using real-world supply chain datasets.
  • Inventory management is crucial for avoiding stockouts, reducing costs, and maintaining high service levels in supply chains.
  • You will learn to transform raw transaction data into actionable insights about stock levels, demand variability, and replenishment needs.
  • We will practice common analytics patterns using realistic retail and manufacturing data.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Core Data Concepts: Transactions, Inventory, Demand, Orders#

  • Operational datasets in supply chain analytics include transactions, inventory snapshots, demand forecasts, and order histories.
  • These datasets are often structured as transaction logs, time series, or event-based records.
  • Beginners often misinterpret date columns, confuse order vs. delivery events, or neglect units of measure.
  • Accurate inventory calculations depend on understanding how movements in and out of stock are recorded.
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  
print(df.columns)
Index(['Invoice', 'StockCode', 'Description', 'Quantity', 'InvoiceDate',
       'Price', 'Customer ID', 'Country'],
      dtype='object')
print(df[['StockCode', 'Description', 'Quantity', 'InvoiceDate']].head(5))
  StockCode                          Description  Quantity         InvoiceDate
0    85123A   WHITE HANGING HEART T-LIGHT HOLDER         6 2010-12-01 08:26:00
1     71053                  WHITE METAL LANTERN         6 2010-12-01 08:26:00
2    84406B       CREAM CUPID HEARTS COAT HANGER         8 2010-12-01 08:26:00
3    84029G  KNITTED UNION FLAG HOT WATER BOTTLE         6 2010-12-01 08:26:00
4    84029E       RED WOOLLY HOTTIE WHITE HEART.         6 2010-12-01 08:26:00
print('Unique stock keeping units (SKUs):', df['StockCode'].nunique())
Unique stock keeping units (SKUs): 4070
daily_demand = df.groupby(['StockCode', df['InvoiceDate'].dt.date])['Quantity'].sum().reset_index()
print(daily_demand.head(5))
  StockCode InvoiceDate  Quantity
0     10002  2010-12-01        60
1     10002  2010-12-02         1
2     10002  2010-12-03         8
3     10002  2010-12-05         1
4     10002  2010-12-06        25
sku = daily_demand['StockCode'].iloc[0]
sku_demand = daily_demand[daily_demand['StockCode'] == sku]
sku_demand = sku_demand.set_index('InvoiceDate').sort_index()
print(sku_demand)
            StockCode  Quantity
InvoiceDate                    
2010-12-01      10002        60
2010-12-02      10002         1
2010-12-03      10002         8
2010-12-05      10002         1
2010-12-06      10002        25
2010-12-07      10002         8
2010-12-08      10002        13
2010-12-09      10002        44
2010-12-10      10002        48
2010-12-13      10002        27
2010-12-14      10002         7
2010-12-16      10002         5
2010-12-17      10002         2
2010-12-20      10002         2
2011-01-05      10002        12
2011-01-06      10002        60
2011-01-07      10002         1
2011-01-11      10002        24
2011-01-13      10002        11
2011-01-16      10002         7
2011-01-17      10002         1
2011-01-18      10002        24
2011-01-19      10002        13
2011-01-20      10002        36
2011-01-23      10002         2
2011-01-24      10002         1
2011-01-25      10002         2
2011-01-30      10002        14
2011-01-31      10002       132
2011-02-04      10002         1
2011-02-13      10002         1
2011-02-17      10002        14
2011-02-22      10002        12
2011-02-25      10002        24
2011-03-01      10002         2
2011-03-04      10002         2
2011-03-07      10002         2
2011-03-15      10002         1
2011-03-17      10002         6
2011-03-20      10002         4
2011-03-21      10002         5
2011-03-25      10002         6
2011-03-30      10002       180
2011-04-01      10002       120
2011-04-03      10002         6
2011-04-15      10002        62
2011-04-18      10002         1
2011-04-28      10002        -3
import matplotlib.pyplot as plt
plt.figure(figsize=(10,4))
sku_demand['Quantity'].plot(marker='o')
plt.title(f'Daily Demand for SKU {sku}')
plt.xlabel('Date')
plt.ylabel('Quantity Sold')
plt.tight_layout()
plt.show()
No description has been provided for this image

Example: Calculating Simple Inventory Position#

  • The inventory position for a product is the on-hand stock after accounting for all inflows and outflows up to a given date.
  • We can simulate inventory depletion by using daily sales as product outflows.
  • Without replenishments, the inventory position only falls.
initial_stock = 300
sku_demand['inventory_position'] = initial_stock - sku_demand['Quantity'].cumsum()
print(sku_demand[['Quantity', 'inventory_position']])
             Quantity  inventory_position
InvoiceDate                              
2010-12-01         60                 240
2010-12-02          1                 239
2010-12-03          8                 231
2010-12-05          1                 230
2010-12-06         25                 205
2010-12-07          8                 197
2010-12-08         13                 184
2010-12-09         44                 140
2010-12-10         48                  92
2010-12-13         27                  65
2010-12-14          7                  58
2010-12-16          5                  53
2010-12-17          2                  51
2010-12-20          2                  49
2011-01-05         12                  37
2011-01-06         60                 -23
2011-01-07          1                 -24
2011-01-11         24                 -48
2011-01-13         11                 -59
2011-01-16          7                 -66
2011-01-17          1                 -67
2011-01-18         24                 -91
2011-01-19         13                -104
2011-01-20         36                -140
2011-01-23          2                -142
2011-01-24          1                -143
2011-01-25          2                -145
2011-01-30         14                -159
2011-01-31        132                -291
2011-02-04          1                -292
2011-02-13          1                -293
2011-02-17         14                -307
2011-02-22         12                -319
2011-02-25         24                -343
2011-03-01          2                -345
2011-03-04          2                -347
2011-03-07          2                -349
2011-03-15          1                -350
2011-03-17          6                -356
2011-03-20          4                -360
2011-03-21          5                -365
2011-03-25          6                -371
2011-03-30        180                -551
2011-04-01        120                -671
2011-04-03          6                -677
2011-04-15         62                -739
2011-04-18          1                -740
2011-04-28         -3                -737
plt.figure(figsize=(10,4))
sku_demand['inventory_position'].plot(marker='s', color='orange')
plt.title(f'Inventory Position for SKU {sku}')
plt.axhline(0, color='red', linestyle='--', label='Stockout Line')
plt.xlabel('Date')
plt.ylabel('Inventory Level')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image

Example: Filling In Missing Dates in Inventory Series#

  • Sales records may not appear on every day, so we must fill missing dates to produce a continuous inventory time series.
  • Missing dates can lead to underestimating how long an item has been out of stock.
all_days = pd.date_range(start=sku_demand.index.min(), end=sku_demand.index.max())
sku_full = sku_demand.reindex(all_days, fill_value=0)
sku_full['inventory_position'] = initial_stock - sku_full['Quantity'].cumsum()
print(sku_full.head(10))
           StockCode  Quantity  inventory_position
2010-12-01     10002        60                 240
2010-12-02     10002         1                 239
2010-12-03     10002         8                 231
2010-12-04         0         0                 231
2010-12-05     10002         1                 230
2010-12-06     10002        25                 205
2010-12-07     10002         8                 197
2010-12-08     10002        13                 184
2010-12-09     10002        44                 140
2010-12-10     10002        48                  92
stockout_days = (sku_full['inventory_position'] <= 0).sum()
print('Number of stockout days:', stockout_days)
Number of stockout days: 113

Intermediate Example: Calculating Rolling Average Demand#

  • Smoother demand estimates help plan reorder points in volatile environments.
  • Rolling averages filter out random spikes and dips.
sku_full['rolling_demand_7d'] = sku_full['Quantity'].rolling(window=7, min_periods=1).mean()
print(sku_full[['Quantity', 'rolling_demand_7d']].head(10))
            Quantity  rolling_demand_7d
2010-12-01        60          60.000000
2010-12-02         1          30.500000
2010-12-03         8          23.000000
2010-12-04         0          17.250000
2010-12-05         1          14.000000
2010-12-06        25          15.833333
2010-12-07         8          14.714286
2010-12-08        13           8.000000
2010-12-09        44          14.142857
2010-12-10        48          19.857143
plt.figure(figsize=(10,4))
sku_full['Quantity'].plot(marker='o', linestyle='-', alpha=0.5, label='Actual Demand')
sku_full['rolling_demand_7d'].plot(color='black', linewidth=2, label='7-Day Rolling Average')
plt.title('Actual vs. 7-Day Rolling Average Demand')
plt.xlabel('Date')
plt.ylabel('Units Sold')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image
replenishment_point = int(sku_full['rolling_demand_7d'].mean() * 3)
print('Suggested reorder point (3x avg 7-day demand):', replenishment_point)
Suggested reorder point (3x avg 7-day demand): 22
import openml
dataset = openml.datasets.get_dataset(43900)
X, y, _, _ = dataset.get_data(dataset_format='dataframe')
manuf_df = X.copy() if y is None else pd.concat([X, y], axis=1)
print(manuf_df.shape)
print(manuf_df.head(3))
(32769, 10)
  ACTION RESOURCE MGR_ID ROLE_ROLLUP_1 ROLE_ROLLUP_2 ROLE_DEPTNAME ROLE_TITLE  \
0      1    39353  85475        117961        118300        123472     117905   
1      1    17183   1540        117961        118343        123125     118536   
2      1    36724  14457        118219        118220        117884     117879   

  ROLE_FAMILY_DESC ROLE_FAMILY ROLE_CODE  
0           117906      290919    117908  
1           118536      308574    118539  
2           267952       19721    117880  

Intermediate Example: Aggregating Inventory by Line or Location#

  • Manufacturers often want to track inventory use by production line, workstation, or location.
  • Grouping and aggregation are key analytics building blocks.
if 'line' in manuf_df.columns and 'quantity_used' in manuf_df.columns:
    agg = manuf_df.groupby('line')['quantity_used'].sum()
    print(agg)
else:
    print('No appropriate columns found. Example only.')
No appropriate columns found. Example only.
safety_stock = sku_full['Quantity'].std() * 2
print('Estimated safety stock (2x daily std deviation):', round(safety_stock, 2))
Estimated safety stock (2x daily std deviation): 45.99

Error Handling Example: Missing or Malformed Data#

  • Inventory analyses can break if sales or supply logs have missing values, unexpected negative numbers, or date gaps.
  • Always validate and clean operational data before analytics!
df.loc[42, 'Quantity'] = np.nan  # Simulate a missing value
missing_qty = df['Quantity'].isna().sum()
print('Number of missing Quantity values:', missing_qty)
Number of missing Quantity values: 1
df['Quantity'] = df['Quantity'].fillna(0)
print('Missing quantities replaced with zero.')
Missing quantities replaced with zero.
bad_dates = df['InvoiceDate'].isna().sum()
print('Number of missing InvoiceDate values:', bad_dates)
Number of missing InvoiceDate values: 0
bad_quantity = (df['Quantity'] < 0).sum()
print('Number of negative Quantity entries:', bad_quantity)
Number of negative Quantity entries: 10624

Advanced Example: Join Inventory Movements to Supplier Performance#

  • For robust supply chain visibility, analysts often join stock records to supplier reliability metrics.
  • This allows us to link inventory shortages to procurement and supplier risks directly.
supplier_dataset = openml.datasets.get_dataset(42125)
supplier_df, _, _, _ = supplier_dataset.get_data(dataset_format='dataframe')
print(supplier_df.shape)
print(supplier_df.head(3))
(9228, 13)
          full_name gender  current_annual_salary  2016_gross_pay_received  \
0    Aarhus, Pam J.      F               69222.18                 71225.98   
1   Aaron, David J.      M               97392.47                103088.48   
2  Aaron, Marsha M.      F              104717.28                107000.24   

   2016_overtime_pay department                          department_name  \
0             416.10        POL                     Department of Police   
1            3326.19        POL                     Department of Police   
2            1353.32        HHS  Department of Health and Human Services   

                                            division assignment_category  \
0  MSB Information Mgmt and Tech Division Records...    Fulltime-Regular   
1         ISB Major Crimes Division Fugitive Section    Fulltime-Regular   
2      Adult Protective and Case Management Services    Fulltime-Regular   

       employee_position_title underfilled_job_title date_first_hired  \
0  Office Services Coordinator                  None       09/22/1986   
1        Master Police Officer                  None       09/12/1988   
2             Social Worker IV                  None       11/19/1989   

   year_first_hired  
0              1986  
1              1988  
2              1989  
if 'StockCode' in supplier_df.columns:
    enriched = pd.merge(sku_full.reset_index(), supplier_df, left_on='StockCode', right_on='StockCode', how='left')
    print(enriched.head(2))
else:
    print('No StockCode to join on in supplier data.')
No StockCode to join on in supplier data.

Advanced Example: Predicting Stockouts with Lead Time Simulation#

  • Predictive inventory analytics simulate when an item will run out, plus the earliest time replenishment is feasible.
  • We can do a basic lead time simulation using expected delivery delays.
lead_time_days = 7
projected_runout_day = sku_full[sku_full['inventory_position'] <= 0].index.min()
today = sku_full.index[0]
if pd.notnull(projected_runout_day):
    reorder_date = projected_runout_day - pd.Timedelta(days=lead_time_days)
    print('Suggested reorder date (based on projected stockout and supplier lead time):', reorder_date)
else:
    print('No projected stockout detected. Inventory is sufficient.' )
Suggested reorder date (based on projected stockout and supplier lead time): 2010-12-30 00:00:00

Best Practices: Inventory Aggregation, Time Series Grouping, KPI Calculation#

  • Always aggregate inventory by business-relevant time intervals (day, week, or period).
  • Grouping by SKU, category, or location exposes blind spots in stock coverage.
  • Calculate KPIs such as stockout duration, replenishment delay, and service level to guide operational improvements.
sku_full['stockout'] = sku_full['inventory_position'] <= 0
service_level = 1 - sku_full['stockout'].mean()
print('Calculated service level: {:.2%}'.format(service_level))
Calculated service level: 24.16%
file_path = 'daily_inventory_positions.csv'
sku_full[['inventory_position']].reset_index().rename(columns={'index':'Date'}).to_csv(file_path, index=False)
print(f'Inventory time series exported to {file_path}')
Inventory time series exported to daily_inventory_positions.csv

Mini Project: From Raw Retail Data to Final Inventory KPI#

  • You now understand how to transform transaction logs into business-ready inventory insights.
  • As an exercise, load any SKU, fill in missing days, simulate running out, and export the results.
  • Use the patterns in this notebook to analyze your organizations products or a new dataset.

Found this useful?

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