| name | procurement-optimization |
| description | When the user wants to optimize procurement decisions, allocate orders across suppliers, or determine optimal order quantities. Also use when the user mentions "order allocation," "supplier portfolio optimization," "lot sizing," "order splitting," "purchase optimization," "EOQ," "sourcing optimization," or "multi-sourcing strategy." For supplier selection, see supplier-selection. For spend analysis, see spend-analysis. |
Procurement Optimization
You are an expert in procurement optimization and decision science. Your goal is to help organizations make optimal purchasing decisions that minimize total costs while meeting requirements for service levels, capacity constraints, and risk management.
Initial Assessment
Before optimizing procurement, understand:
-
Procurement Context
- What products/materials are being procured?
- Current procurement process and pain points?
- Spend volume and frequency?
- Number of suppliers and current allocation?
-
Business Objectives
- Primary goal? (cost, service, risk, sustainability)
- Cost components? (price, freight, duties, carrying)
- Service level requirements?
- Risk tolerance?
-
Constraints
- Supplier capacity limits?
- Minimum order quantities (MOQs)?
- Lead times and delivery windows?
- Budget or working capital limits?
- Quality or certification requirements?
-
Data Availability
- Historical demand and order patterns?
- Supplier pricing (including volume discounts)?
- Inventory carrying costs?
- Order processing costs?
- Transportation and logistics costs?
Procurement Optimization Framework
Key Decision Areas
1. Order Quantity Decisions
- Economic Order Quantity (EOQ)
- Lot-sizing with constraints
- Quantity discount optimization
- Joint replenishment
2. Supplier Allocation Decisions
- Single vs. multi-sourcing
- Order splitting across suppliers
- Portfolio optimization
- Supplier diversification
3. Timing Decisions
- Reorder points
- Order scheduling
- Lead time management
- Safety stock levels
4. Contract Decisions
- Fixed vs. flexible quantities
- Price vs. volume commitments
- Long-term vs. spot buying
- Options and hedging
Economic Order Quantity (EOQ)
Classic EOQ Model
Assumptions:
- Constant demand rate
- Instantaneous replenishment
- No stockouts
- Fixed ordering cost and carrying cost
Formula:
EOQ = √(2 × D × S / H)
Where:
D = Annual demand (units)
S = Fixed ordering cost per order
H = Annual holding cost per unit
import numpy as np
import matplotlib.pyplot as plt
def economic_order_quantity(annual_demand, order_cost, holding_cost_rate, unit_cost):
"""
Calculate Economic Order Quantity
Parameters:
- annual_demand: units per year
- order_cost: fixed cost per order ($)
- holding_cost_rate: % of unit cost (e.g., 0.25 for 25%)
- unit_cost: cost per unit ($)
Returns:
- EOQ, total annual cost, number of orders
"""
holding_cost_per_unit = unit_cost * holding_cost_rate
eoq = np.sqrt((2 * annual_demand * order_cost) / holding_cost_per_unit)
num_orders = annual_demand / eoq
ordering_cost = num_orders * order_cost
holding_cost = (eoq / 2) * holding_cost_per_unit
purchase_cost = annual_demand * unit_cost
total_cost = ordering_cost + holding_cost + purchase_cost
return {
'eoq': round(eoq, 0),
'num_orders_per_year': round(num_orders, 1),
'order_frequency_days': round(365 / num_orders, 1),
'total_annual_cost': round(total_cost, 2),
'ordering_cost': round(ordering_cost, 2),
'holding_cost': round(holding_cost, 2),
'purchase_cost': round(purchase_cost, 2)
}
result = economic_order_quantity(
annual_demand=10000,
order_cost=,
holding_cost_rate=,
unit_cost=
)
()
()
()
EOQ with Quantity Discounts
All-Units Discount:
- Price break at certain volume levels
- All units purchased at discounted price
def eoq_quantity_discounts(annual_demand, order_cost, holding_cost_rate, price_breaks):
"""
EOQ with all-units quantity discounts
price_breaks: list of (quantity, unit_price) tuples
Example: [(0, 50), (500, 48), (1000, 46)]
"""
price_breaks = sorted(price_breaks, key=lambda x: x[0])
best_option = None
best_cost = float('inf')
for i, (min_qty, unit_price) in enumerate(price_breaks):
holding_cost = unit_price * holding_cost_rate
eoq = np.sqrt((2 * annual_demand * order_cost) / holding_cost)
if i < len(price_breaks) - 1:
max_qty = price_breaks[i + 1][0] - 1
else:
max_qty = float('inf')
if eoq < min_qty:
order_qty = min_qty
elif eoq > max_qty:
continue
else:
order_qty = eoq
num_orders = annual_demand / order_qty
ordering_cost = num_orders * order_cost
holding_cost_annual = (order_qty / 2) * holding_cost
purchase_cost = annual_demand * unit_price
total_cost = ordering_cost + holding_cost_annual + purchase_cost
if total_cost < best_cost:
best_cost = total_cost
best_option = {
'order_quantity': round(order_qty, ),
: unit_price,
: (num_orders, ),
: (total_cost, ),
: (ordering_cost, ),
: (holding_cost_annual, ),
: (purchase_cost, )
}
best_option
price_breaks = [
(, ),
(, ),
(, ),
(, )
]
result = eoq_quantity_discounts(
annual_demand=,
order_cost=,
holding_cost_rate=,
price_breaks=price_breaks
)
()
()
()
()
()
()
Supplier Allocation Optimization
Multi-Sourcing Problem
Objective:
Allocate orders across multiple suppliers to minimize total cost while meeting capacity, quality, and risk constraints.
Mathematical Formulation:
Decision Variables:
x_i = quantity ordered from supplier i
Objective:
Minimize: Σ (p_i × x_i + f_i × y_i + t_i × x_i)
Where:
p_i = unit price from supplier i
f_i = fixed ordering cost from supplier i
t_i = transportation cost per unit from supplier i
y_i = binary (1 if order from supplier i, 0 otherwise)
Constraints:
Σ x_i >= D (meet demand)
x_i <= C_i × y_i (supplier capacity)
x_i >= MOQ_i × y_i (minimum order quantity)
Σ (q_i × x_i) / Σ x_i >= Q (average quality requirement)
x_i / Σ x_i <= R_max (diversification - max % per supplier)
from pulp import *
import pandas as pd
def optimize_supplier_allocation(suppliers, demand, constraints=None):
"""
Optimize order allocation across multiple suppliers
suppliers: DataFrame with columns:
- supplier_id, unit_price, fixed_cost, capacity, moq,
transport_cost, quality_score, lead_time
demand: total quantity needed
constraints: dict with optional keys:
- min_quality: minimum average quality score
- max_supplier_share: max % of demand from one supplier
- max_suppliers: maximum number of suppliers to use
"""
if constraints is None:
constraints = {}
prob = LpProblem("Supplier_Allocation", LpMinimize)
x = LpVariable.dicts("Quantity",
suppliers.index,
lowBound=0,
cat='Continuous')
y = LpVariable.dicts("Use",
suppliers.index,
cat='Binary')
prob += (
lpSum([(suppliers.loc[i, 'unit_price'] +
suppliers.loc[i, 'transport_cost']) * x[i]
for i in suppliers.index]) +
lpSum([suppliers.loc[i, 'fixed_cost'] * y[i]
for i in suppliers.index])
)
prob += lpSum([x[i] for i suppliers.index]) >= demand,
i suppliers.index:
prob += x[i] <= suppliers.loc[i, ] * y[i],
i suppliers.index:
prob += x[i] >= suppliers.loc[i, ] * y[i],
constraints:
prob += (
lpSum([suppliers.loc[i, ] * x[i]
i suppliers.index]) >=
constraints[] * demand,
)
constraints:
i suppliers.index:
prob += (
x[i] <= constraints[] * demand,
)
constraints:
prob += (
lpSum([y[i] i suppliers.index]) <=
constraints[],
)
prob.solve(PULP_CBC_CMD(msg=))
LpStatus[prob.status] != :
{: LpStatus[prob.status], : }
results = []
i suppliers.index:
x[i].varValue > :
qty = x[i].varValue
unit_cost = (suppliers.loc[i, ] +
suppliers.loc[i, ])
variable_cost = qty * unit_cost
fixed_cost = suppliers.loc[i, ]
total_cost = variable_cost + fixed_cost
results.append({
: suppliers.loc[i, ],
: (qty, ),
: (qty / demand * , ),
: unit_cost,
: (variable_cost, ),
: fixed_cost,
: (total_cost, ),
: suppliers.loc[i, ],
: suppliers.loc[i, ]
})
results_df = pd.DataFrame(results)
results_df = results_df.sort_values()
{
: ,
: (value(prob.objective), ),
: results_df,
: (
(results_df[] * results_df[]).() /
results_df[].(),
),
: (results_df)
}
suppliers_data = pd.DataFrame({
: [, , , ],
: [, , , ],
: [, , , ],
: [, , , ],
: [, , , ],
: [, , , ],
: [, , , ],
: [, , , ]
})
result = optimize_supplier_allocation(
suppliers=suppliers_data,
demand=,
constraints={
: ,
: ,
:
}
)
()
()
()
()
()
(result[])
Portfolio Optimization Approach
Efficient Frontier:
Trade-off between cost and risk (supplier diversification)
import numpy as np
from scipy.optimize import minimize
def supplier_portfolio_optimization(suppliers_df, demand,
risk_aversion=0.5):
"""
Optimize supplier portfolio considering cost and risk
suppliers_df: DataFrame with unit_cost, std_dev (cost volatility)
risk_aversion: 0 = cost only, 1 = risk only, 0.5 = balanced
"""
n_suppliers = len(suppliers_df)
def objective(weights):
"""Minimize weighted combination of cost and risk"""
expected_cost = np.sum(
weights * suppliers_df['unit_cost'].values * demand
)
cost_variance = np.sum(
(weights * demand) ** 2 * suppliers_df['std_dev'].values ** 2
)
cost_risk = np.sqrt(cost_variance)
return (1 - risk_aversion) * expected_cost + risk_aversion * cost_risk
constraints = [
{'type': 'eq', 'fun': lambda w: np.sum(w) - 1},
]
bounds = [(0, 0.6) for _ in range(n_suppliers)]
x0 = np.ones(n_suppliers) / n_suppliers
result = minimize(objective, x0, method=,
bounds=bounds, constraints=constraints)
result.success:
weights = result.x
allocation = weights * demand
{
: weights,
: allocation,
: np.(weights * suppliers_df[].values * demand),
: np.sqrt(np.((weights * demand) ** *
suppliers_df[].values ** ))
}
:
Advanced Procurement Models
Joint Replenishment Problem (JRP)
Multiple Items from Same Supplier:
- Share fixed ordering cost
- Coordinate order timing
- Minimize total cost
def joint_replenishment_problem(items_df, shared_fixed_cost):
"""
Joint replenishment for multiple items
items_df: DataFrame with annual_demand, unit_cost, holding_cost_rate
shared_fixed_cost: fixed cost incurred per joint order
"""
items_df['individual_eoq'] = np.sqrt(
(2 * items_df['annual_demand'] * shared_fixed_cost) /
(items_df['unit_cost'] * items_df['holding_cost_rate'])
)
items_df['frequency'] = items_df['annual_demand'] / items_df['individual_eoq']
base_frequency = items_df['frequency'].max()
items_df['assigned_frequency'] = items_df['frequency'].apply(
lambda f: base_frequency / (2 ** round(np.log2(base_frequency / f)))
)
items_df['order_quantity'] = (
items_df['annual_demand'] / items_df['assigned_frequency']
)
items_df['ordering_cost'] = (
shared_fixed_cost * items_df['assigned_frequency'] / len(items_df)
)
items_df['holding_cost'] = (
items_df['order_quantity'] / 2 *
items_df['unit_cost'] *
items_df['holding_cost_rate']
)
items_df['total_cost'] = (
items_df['ordering_cost'] +
items_df[] +
items_df[] * items_df[]
)
joint_order_frequency = base_frequency
days_between_orders = / joint_order_frequency
{
: items_df,
: (joint_order_frequency, ),
: (days_between_orders, ),
: (items_df[].(), )
}
items = pd.DataFrame({
: [, , ],
: [, , ],
: [, , ],
: [, , ]
})
result = joint_replenishment_problem(items, shared_fixed_cost=)
()
()
()
(result[][[, , ]])
Dynamic Lot Sizing (Wagner-Whitin)
Time-Varying Demand:
- Demand varies by period
- No backorders
- Minimize total cost over planning horizon
def wagner_whitin(demands, setup_cost, holding_cost_per_unit):
"""
Wagner-Whitin algorithm for dynamic lot sizing
demands: list of demands by period [d1, d2, d3, ...]
setup_cost: fixed cost per order
holding_cost_per_unit: cost to hold 1 unit for 1 period
Returns: optimal order quantities and total cost
"""
n_periods = len(demands)
cost = [float('inf')] * (n_periods + 1)
cost[0] = 0
order_in = [0] * (n_periods + 1)
for t in range(1, n_periods + 1):
for s in range(0, t):
cum_demand = sum(demands[s:t])
hold_cost = sum(
(t - k - 1) * demands[k] * holding_cost_per_unit
for k in range(s, t)
)
total_cost = cost[s] + setup_cost + hold_cost
if total_cost < cost[t]:
cost[t] = total_cost
order_in[t] = s + 1
orders = [0] * n_periods
period = n_periods
while period > 0:
order_period = order_in[period]
order_qty = sum(demands[order_period - 1:period])
orders[order_period - ] = order_qty
period = order_period -
{
: orders,
: cost[n_periods],
: ( q orders q > )
}
demands = [, , , , , ]
setup_cost =
holding_cost =
result = wagner_whitin(demands, setup_cost, holding_cost)
()
t, qty (result[], ):
qty > :
()
()
()
Procurement Risk Management
Supply Risk Metrics
def calculate_supply_risk_score(supplier_data):
"""
Calculate comprehensive supply risk score
supplier_data: dict with risk factors
Returns: risk score (0-100, higher = riskier)
"""
risk_score = 0
factors = []
spend_concentration = supplier_data.get('spend_share', 0)
if spend_concentration > 0.5:
risk_score += 25
factors.append("High spend concentration")
elif spend_concentration > 0.3:
risk_score += 15
factors.append("Moderate spend concentration")
if supplier_data.get('single_location', False):
risk_score += 15
factors.append("Single location risk")
if supplier_data.get('geopolitical_risk', False):
risk_score += 20
factors.append("Geopolitical risk")
financial_score = supplier_data.get('financial_health', 7)
if financial_score < 5:
risk_score += 20
factors.append("Poor financial health")
elif financial_score < 7:
risk_score += 10
factors.append("Moderate financial concerns")
capacity_util = supplier_data.get('capacity_utilization', )
capacity_util > :
risk_score +=
factors.append()
capacity_util > :
risk_score +=
factors.append()
defect_rate = supplier_data.get(, )
defect_rate > :
risk_score +=
factors.append()
defect_rate > :
risk_score +=
otd_rate = supplier_data.get(, )
otd_rate < :
risk_score +=
factors.append()
otd_rate < :
risk_score +=
risk_level = risk_score < risk_score <
{
: risk_score,
: risk_level,
: factors
}
Optimal Dual Sourcing
Balance cost vs. risk:
def optimal_dual_sourcing(primary_supplier, backup_supplier,
annual_demand, disruption_prob, disruption_cost):
"""
Determine optimal split between primary and backup supplier
Primary supplier: lower cost, higher risk
Backup supplier: higher cost, lower risk
"""
best_split = None
best_expected_cost = float('inf')
for primary_pct in range(50, 101, 5):
backup_pct = 100 - primary_pct
primary_qty = annual_demand * (primary_pct / 100)
backup_qty = annual_demand * (backup_pct / 100)
primary_cost = primary_qty * primary_supplier['unit_cost']
backup_cost = backup_qty * backup_supplier['unit_cost']
expected_disruption = (
disruption_prob *
(primary_pct / 100) *
disruption_cost
)
total_expected_cost = primary_cost + backup_cost + expected_disruption
if total_expected_cost < best_expected_cost:
best_expected_cost = total_expected_cost
best_split = {
'primary_pct': primary_pct,
'backup_pct': backup_pct,
'primary_qty': round(primary_qty, 0),
'backup_qty': round(backup_qty, 0),
'primary_cost': round(primary_cost, 2),
: (backup_cost, ),
: (expected_disruption, ),
: (total_expected_cost, )
}
best_split
primary = {: }
backup = {: }
result = optimal_dual_sourcing(
primary_supplier=primary,
backup_supplier=backup,
annual_demand=,
disruption_prob=,
disruption_cost=
)
()
()
()
()
Tools & Libraries
Python Libraries
Optimization:
pulp: Linear programming (supplier allocation, lot sizing)
scipy.optimize: General optimization (portfolio, dual sourcing)
pyomo: Advanced optimization modeling
cvxpy: Convex optimization
ortools: Google OR-Tools (constraint programming)
Data Analysis:
pandas: Data manipulation
numpy: Numerical computations
statsmodels: Statistical analysis
Visualization:
matplotlib, seaborn: Charts and plots
plotly: Interactive dashboards
Commercial Software
Procurement Optimization:
- SAP Ariba: Strategic sourcing and procurement
- Coupa: Source-to-pay platform
- Jaggaer: Strategic sourcing suite
- GEP SMART: Unified procurement
- PROS: Price and profit optimization
- Keelvar: Sourcing optimization
Supply Chain Optimization:
- LLamasoft: Supply chain design and optimization
- Blue Yonder: Supply chain planning
- o9 Solutions: Integrated planning
- Kinaxis RapidResponse: S&OP platform
Analytics:
- Tableau, Power BI: Procurement dashboards
- SpendHQ: Spend analytics
- Zycus: Spend analysis
Common Challenges & Solutions
Challenge: Quantity Discount Complexity
Problem:
- Multiple price breaks
- Different discount structures per supplier
- Hard to compare apples-to-apples
Solutions:
- Use optimization to evaluate all combinations
- Calculate total landed cost including carrying
- Sensitivity analysis on demand uncertainty
- Consider cash flow impact of large orders
Challenge: Minimum Order Quantities (MOQs)
Problem:
- MOQs create excess inventory
- May force use of non-optimal suppliers
- Conflicts with JIT goals
Solutions:
- Negotiate lower MOQs with volume commitments
- Joint orders with other business units
- Consolidate similar items
- Evaluate total cost including holding costs
- Use contract manufacturers or distributors
Challenge: Lead Time Variability
Problem:
- Uncertain delivery times
- Impacts safety stock needs
- Complicates order timing
Solutions:
- Model lead time as probability distribution
- Optimize reorder points under uncertainty
- Diversify suppliers by geography
- Implement vendor-managed inventory (VMI)
- Use tracking and visibility tools
Challenge: Multi-Objective Trade-offs
Problem:
- Conflicting goals (cost, risk, quality, sustainability)
- Different stakeholder priorities
- Hard to quantify some objectives
Solutions:
- Multi-criteria decision analysis (weighted scoring)
- Pareto optimization (efficient frontier)
- Scenario analysis showing trade-offs
- Stakeholder workshops to align priorities
- Set constraints on secondary objectives
Challenge: Demand Uncertainty
Problem:
- Forecast errors lead to over/under ordering
- Optimal order quantity changes with demand
- Risk of obsolescence or stockouts
Solutions:
- Use expected demand in EOQ calculations
- Safety stock optimization
- Flexible contracts (options, postponement)
- Vendor-managed inventory (VMI)
- Periodic review and adjustment
- Risk pooling through postponement
Output Format
Procurement Optimization Report
Executive Summary:
- Recommended procurement strategy
- Total cost and savings opportunity
- Key changes from current approach
- Implementation requirements
Optimal Order Allocation:
| Supplier | Allocation | Share % | Unit Cost | Total Cost | Quality | Lead Time | Risk Level |
|---|
| Supplier B | 4,800 units | 60% | $11.00 | $52,800 | 9/10 | 21 days | Low |
| Supplier C | 2,400 units | 30% | $11.30 | $27,120 | 10/10 | 14 days | Low |
| Supplier D | 800 units | 10% | $11.00 | $8,800 | 8.5/10 | 21 days | Medium |
| Total | 8,000 units | 100% | $11.09 | $88,720 | 9.2/10 | 19 days | Low |
Cost Breakdown:
| Component | Current | Optimized | Savings | % Change |
|---|
| Purchase Price | $95,000 | $88,000 | $7,000 | -7.4% |
| Transportation | $12,000 | $10,400 | $1,600 | -13.3% |
| Ordering Costs | $2,400 | $1,350 | $1,050 | -43.8% |
| Holding Costs | $18,000 | $15,500 | $2,500 | -13.9% |
| Total | $127,400 | $115,250 | $12,150 | -9.5% |
Order Schedule:
Recommended Order Plan (Next 12 Months):
Q1:
- Order 1,200 units from Supplier B (Week 1)
- Order 600 units from Supplier C (Week 1)
- Order 200 units from Supplier D (Week 1)
Q2:
- Order 1,200 units from Supplier B (Week 14)
- Order 600 units from Supplier C (Week 14)
Q3:
- Order 1,200 units from Supplier B (Week 27)
- Order 600 units from Supplier C (Week 27)
- Order 300 units from Supplier D (Week 27)
Q4:
- Order 1,200 units from Supplier B (Week 40)
- Order 600 units from Supplier C (Week 40)
- Order 300 units from Supplier D (Week 40)
Risk Assessment:
- Overall supply risk: Low
- No single supplier >60% of volume (diversified)
- Average supplier financial health: 8.5/10 (strong)
- Geographic diversification: 3 regions
- Quality performance: 99.1% defect-free (excellent)
Recommendations:
- Transition to 60/30/10 split across three suppliers
- Implement quarterly orders to balance ordering and holding costs
- Negotiate 2-year contracts with volume commitments for price stability
- Establish performance KPIs and quarterly reviews
- Maintain qualified backup supplier (Supplier A) for emergencies
Implementation Plan:
- Month 1: Finalize contracts with selected suppliers
- Month 2: Place initial orders and validate quality
- Month 3: Ramp to full production volumes
- Month 4+: Monitor performance and adjust as needed
Questions to Ask
If you need more context:
- What products/materials are being procured?
- What's the annual demand volume and variability?
- How many suppliers are available and what are their capabilities?
- What are the key cost drivers? (unit price, transportation, holding)
- Any constraints? (MOQs, capacity limits, quality requirements)
- What's the current procurement approach and pain points?
- What's more important: lowest cost, risk mitigation, or quality?
- Are there quantity discounts or price breaks?
- What lead times and delivery performance do suppliers offer?
- Is this a one-time purchase or ongoing replenishment?
Related Skills
- supplier-selection: For evaluating and selecting suppliers
- strategic-sourcing: For category strategy and sourcing approach
- spend-analysis: For analyzing spend patterns and opportunities
- inventory-optimization: For safety stock and reorder points
- supplier-risk-management: For monitoring supplier risks
- contract-management: For negotiating optimal contract terms
- demand-forecasting: For demand inputs to procurement planning