Inventory Optimization in Python — Deep Dive
Production inventory optimization goes beyond EOQ into stochastic models that account for uncertain demand, variable lead times, multi-echelon networks, and budget constraints linking thousands of SKUs. This guide covers the models and Python implementations that power real warehouse systems.
Stochastic inventory models
Continuous review (Q, R) policy
The system monitors stock continuously and places a fixed order of Q units whenever inventory drops to the reorder point R.
import numpy as np
from scipy.stats import norm
def continuous_review_policy(
avg_demand_per_day: float,
std_demand_per_day: float,
lead_time_days: float,
std_lead_time: float,
order_cost: float,
holding_cost_per_unit: float,
service_level: float = 0.95,
) -> dict:
annual_demand = avg_demand_per_day * 365
eoq = np.sqrt(2 * annual_demand * order_cost / holding_cost_per_unit)
# Demand during lead time
mean_dlt = avg_demand_per_day * lead_time_days
std_dlt = np.sqrt(
lead_time_days * std_demand_per_day**2
+ avg_demand_per_day**2 * std_lead_time**2
)
z = norm.ppf(service_level)
safety_stock = z * std_dlt
reorder_point = mean_dlt + safety_stock
return {
"order_quantity": round(eoq),
"reorder_point": round(reorder_point),
"safety_stock": round(safety_stock),
"expected_orders_per_year": round(annual_demand / eoq, 1),
}
The key insight is that safety stock depends on both demand variability and lead time variability. A supplier who delivers in “5 to 15 days” requires far more buffer than one who delivers in “9 to 11 days” even if the average is the same.
Periodic review (S, s) policy
Stock is checked at fixed intervals (e.g., every Monday). If inventory is below the threshold s, order up to level S. This suits businesses where ordering happens on a schedule.
def periodic_review_policy(
avg_demand_per_day: float,
std_demand_per_day: float,
review_period_days: int,
lead_time_days: float,
service_level: float = 0.95,
) -> dict:
protection_interval = review_period_days + lead_time_days
mean_demand_pi = avg_demand_per_day * protection_interval
std_demand_pi = std_demand_per_day * np.sqrt(protection_interval)
z = norm.ppf(service_level)
order_up_to = mean_demand_pi + z * std_demand_pi
return {
"order_up_to_level": round(order_up_to),
"review_period_days": review_period_days,
"safety_stock": round(z * std_demand_pi),
}
Newsvendor model for perishables
When products expire or become worthless after a single selling period (newspapers, fashion, fresh food), the newsvendor model finds the optimal stocking quantity:
def newsvendor_quantity(
cost: float,
price: float,
salvage: float,
demand_mean: float,
demand_std: float,
) -> int:
cu = price - cost # cost of understocking
co = cost - salvage # cost of overstocking
critical_ratio = cu / (cu + co)
optimal_q = norm.ppf(critical_ratio, loc=demand_mean, scale=demand_std)
return max(0, int(np.ceil(optimal_q)))
A bakery selling croissants at €3.00 each, costing €1.00 to make, with €0.20 salvage value, has a critical ratio of 2.0 / 2.8 = 0.714. If daily demand is Normal(100, 20), optimal stock is about 111 units.
Multi-SKU optimization under budget constraints
When a warehouse has limited capital, optimizing each SKU independently can exceed the budget. A constrained approach:
from scipy.optimize import minimize
def multi_sku_optimize(skus: list[dict], budget: float) -> list[float]:
"""
Each SKU dict has: cost, demand_mean, demand_std, price, holding_rate.
Returns optimal order quantities under total budget constraint.
"""
n = len(skus)
def total_expected_profit(quantities):
profit = 0
for i, q in enumerate(quantities):
s = skus[i]
expected_sales = min(q, s["demand_mean"])
expected_leftover = max(0, q - s["demand_mean"])
profit += s["price"] * expected_sales - s["cost"] * q - s["holding_rate"] * expected_leftover
return -profit # minimize negative profit
constraints = [{
"type": "ineq",
"fun": lambda q: budget - sum(q[i] * skus[i]["cost"] for i in range(n)),
}]
bounds = [(0, s["demand_mean"] * 3) for s in skus]
x0 = [s["demand_mean"] for s in skus]
result = minimize(total_expected_profit, x0, bounds=bounds, constraints=constraints, method="SLSQP")
return [round(q) for q in result.x]
For thousands of SKUs, this becomes a large-scale optimization. Decomposition by product category or Lagrangian relaxation of the budget constraint scales better than monolithic SLSQP.
ABC-XYZ classification
Combining revenue ranking (ABC) with demand variability (XYZ) produces a 9-cell matrix:
| X (stable) | Y (variable) | Z (erratic) | |
|---|---|---|---|
| A | Lean stock, tight control | Moderate buffer, weekly review | Significant buffer, demand sensing |
| B | Standard EOQ | Periodic review | Safety stock heavy |
| C | Min-max simple rules | Reorder when empty | Consider make-to-order |
import pandas as pd
def abc_xyz_classify(df: pd.DataFrame) -> pd.DataFrame:
# ABC by annual revenue
df = df.sort_values("annual_revenue", ascending=False)
df["cum_rev_pct"] = df["annual_revenue"].cumsum() / df["annual_revenue"].sum()
df["abc"] = pd.cut(df["cum_rev_pct"], bins=[0, 0.8, 0.95, 1.0], labels=["A", "B", "C"])
# XYZ by coefficient of variation
df["cv"] = df["demand_std"] / df["demand_mean"]
df["xyz"] = pd.cut(df["cv"], bins=[0, 0.5, 1.0, float("inf")], labels=["X", "Y", "Z"])
df["class"] = df["abc"].astype(str) + df["xyz"].astype(str)
return df
Multi-echelon inventory
Large supply chains have multiple stock points: a central warehouse feeds regional hubs that feed retail stores. Optimizing each level independently creates the bullwhip effect — small demand swings at retail amplify into wild ordering swings upstream.
Multi-echelon optimization jointly sets safety stocks across all levels to minimize total inventory while meeting end-customer service levels. The inventoryanalytics Python package implements the Clark-Scarf model for serial systems:
# pip install inventoryanalytics
from inventoryanalytics.lotsizing.stochastic import newsvendor
# For serial supply chains, echelon stock policies
# coordinate ordering across warehouse → hub → store layers
For tree-structured networks (one warehouse, many stores), the Guaranteed Service Model (GSM) is the standard approach. Implementation requires solving a dynamic program over the network graph.
Simulation-based validation
Before deploying a new inventory policy, simulate it against historical demand:
def simulate_policy(daily_demand: list[int], reorder_point: int, order_qty: int, lead_time: int) -> dict:
inventory = order_qty
on_order = []
stockouts = 0
total_holding = 0
for day, demand in enumerate(daily_demand):
# Receive arrivals
on_order = [(qty, eta - 1) for qty, eta in on_order]
for qty, eta in on_order:
if eta <= 0:
inventory += qty
on_order = [(qty, eta) for qty, eta in on_order if eta > 0]
# Fulfill demand
if inventory >= demand:
inventory -= demand
else:
stockouts += demand - inventory
inventory = 0
# Reorder check
if inventory <= reorder_point and not on_order:
on_order.append((order_qty, lead_time))
total_holding += inventory
service_level = 1 - stockouts / sum(daily_demand)
avg_inventory = total_holding / len(daily_demand)
return {"service_level": round(service_level, 4), "avg_inventory": round(avg_inventory, 1), "total_stockouts": stockouts}
Running this over two years of data reveals whether the analytical model’s assumptions hold. If simulated service level differs significantly from the target, adjust safety stock multipliers.
Pitfalls
- Ignoring lead time variability — many implementations use a fixed lead time. Supplier delays are the primary cause of stockouts, not demand spikes.
- Confusing fill rate with cycle service level — fill rate measures what fraction of units ship on time; cycle service level measures what fraction of order cycles have no stockout. A 95% cycle service level might correspond to only 98-99% fill rate.
- Static parameters — demand patterns shift. Re-estimate parameters monthly and alert when coefficient of variation changes significantly.
- Over-optimizing C items — the analyst time spent fine-tuning safety stock for low-revenue SKUs often exceeds the savings. Simple rules suffice.
The one thing to remember: Production inventory optimization combines stochastic demand models, budget-constrained multi-SKU solvers, and simulation validation to keep service levels high while minimizing the capital trapped in warehouses.
See Also
- Python Adaptive Learning Systems How Python builds learning apps that adjust to each student like a personal tutor who knows exactly what you need next.
- Python Airflow Learn Airflow as a timetable manager that makes sure data tasks run in the right order every day.
- Python Altair Learn Altair through the idea of drawing charts by describing rules, not by hand-placing every visual element.
- Python Automated Grading How Python grades homework and exams automatically, from simple answer keys to understanding written essays.
- Python Batch Vs Stream Processing Batch processing is like doing laundry once a week; stream processing is like a self-cleaning shirt that cleans itself constantly.