lot-sizing-problems について
複数期間にわたる発注量や生産量を最適化し、段取り・在庫保有・場合によっては欠品コストを抑えるスキルです。生産・在庫計画向けに、ロットサイズ方針や最適化モデル、能力制約と需要の不確実性を扱います。
主な機能
- 01複数期間の数量計画
- 02段取り費と保管費の最小化
- 03ロットサイズ方針の比較
- 04能力制約付きモデル
- 05需要の不確実性への指針
活用シーン
- 01期間ごとの生産計画
- 02段取り費と在庫費の均衡
- 03生産能力の制約への対応
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill lot-sizing-problemsインストール後、Claude Code で/lot-sizing-problemsを入力して読み込まれたか確認します。
詳細
ロットサイジング問題
複数期間のロットサイジングモデルと、生産・在庫計画の最適化に精通しています。目標は、セットアップ費用、在庫保管費用、および場合によっては欠品費用を含む総費用を最小化するため、複数の期間にわたる最適な発注量または生産量を決定することです。
初期評価
ロットサイジング問題を解く前に、次の点を把握してください。
-
計画のコンテキスト
- 計画期間はどれくらいですか?(週、月、四半期)
- ローリング計画ですか、それとも固定期間計画ですか?
- 需要パターンはどのようなものですか?(確定的、予測、実際の受注)
- リードタイムを考慮する必要はありますか?
-
費用構造
- 1回あたりのセットアップ費用/発注費用($)はいくらですか?
- 1単位・1期間あたりの在庫保管費用($/unit/period)はいくらですか?
- 1単位あたりの生産費用/購入費用はいくらですか?
- バックオーダーまたは欠品の費用はありますか?
- 費用構造は期間によって変動しますか?
-
能力と制約
- 期間ごとの生産能力の上限はありますか?
- 保管能力の上限はありますか?
- 最小ロットサイズやバッチに関する制約はありますか?
- 複数の品目が能力を共有していますか?
-
製品の特性
- 単一品目ですか、それとも複数品目ですか?
- 部品表(BOM)の構造はどのようになっていますか?
- 製品間の代替可能性はありますか?
- 製品寿命や陳腐化の問題はありますか?
-
運用上の要件
- バックオーダーは発生しますか?
- 需要は直ちに満たす必要がありますか?
- 多段階生産(BOM)ですか?
- サプライヤーの制約(MOQ、リードタイム)はありますか?
ロットサイジング問題の基本
問題の定義
次の条件が与えられています。
- 計画期間:T 期間(t = 1, 2, ..., T)
- 各期間の需要:D_t(確定的に既知)
- セットアップ費用:S(発注/生産時に発生する固定費用)
- 在庫保管費用:1単位・1期間あたり h
- 単価:c(一定の場合は無視されることが多い)
意思決定:
- いつ発注/生産するか?
- 各期間にどれだけ発注/生産するか?
目的:
- 総費用を最小化する = セットアップ費用 + 在庫保管費用
主なロットサイジング方針
1. ロット・フォー・ロット(L4L)
- 各期間に必要な分だけ発注します:Q_t = D_t
- 在庫保管費用を最小化します(在庫ゼロ)
- セットアップ費用を最大化します
- セットアップ費用が非常に低い場合に使用します
2. 定量発注方式(FOQ)
- 毎回同じ数量 Q を発注します
- 実装が簡単です
- 需要パターンにうまく適合しない場合があります
3. 経済的発注量(EOQ)
- 平均需要を用いて EOQ の公式を使用します
- 需要率が一定であることを前提とします
4. 期間別発注量(POQ)
- N 期間ごとに発注します(N は EOQ によって決定します)
- 複数期間の需要をまとめます
5. 最小単位費用(LUC)
- 単位あたりの費用を最小化するロットサイズを選択します
- 先読み型のヒューリスティックです
6. 最小総費用(LTC)/期間部品バランス法
- セットアップ費用と在庫保管費用のバランスを取ります
- 累積在庫保管費用がセットアップ費用とほぼ等しくなるタイミングでロットを選択します
7. Silver-Meal ヒューリスティック
- 期間あたりの平均費用を最小化します
- 実用的で広く使われているヒューリスティックです
8. Wagner-Whitin アルゴリズム
- 動的計画法によるアプローチです
- 容量制約のない問題の最適解を求めます
- 多項式時間計算量 O(T²) です
Python による実装:ロットサイジングモデル
基本的なロットサイジングのヒューリスティック
import numpy as np
import pandas as pd
from typing import List, Dict, Tuple
import matplotlib.pyplot as plt
class LotSizingProblem:
"""
Multi-period lot-sizing problem solver
Implements various heuristics and optimal algorithm
"""
def __init__(self, demands: List[float], setup_cost: float,
holding_cost: float, unit_cost: float = 0):
"""
Parameters:
-----------
demands : list
Demand for each period [D1, D2, ..., DT]
setup_cost : float
Fixed cost incurred when placing order/production run
holding_cost : float
Cost per unit held in inventory per period
unit_cost : float
Variable cost per unit (often omitted if constant)
"""
self.demands = np.array(demands)
self.T = len(demands)
self.S = setup_cost
self.h = holding_cost
self.c = unit_cost
def lot_for_lot(self) -> Dict:
"""
Lot-for-Lot (L4L) policy: Order exactly demand each period
Minimizes inventory but incurs setup cost every period
"""
orders = self.demands.copy()
inventory = np.zeros(self.T + 1) # Inventory at end of each period
setup_costs = np.zeros(self.T)
holding_costs = np.zeros(self.T)
for t in range(self.T):
# Order arrives at start of period
inventory[t] += orders[t]
# Satisfy demand
inventory[t] -= self.demands[t]
# Costs
if orders[t] > 0:
setup_costs[t] = self.S
holding_costs[t] = inventory[t] * self.h
# Carry inventory forward
inventory[t + 1] = inventory[t]
total_setup = setup_costs.sum()
total_holding = holding_costs.sum()
total_cost = total_setup + total_holding
return {
'method': 'Lot-for-Lot',
'orders': orders,
'inventory': inventory[:-1],
'setup_cost': total_setup,
'holding_cost': total_holding,
'total_cost': total_cost,
'num_orders': (orders > 0).sum()
}
def fixed_order_quantity(self, Q: float) -> Dict:
"""
Fixed Order Quantity: Order Q units whenever inventory insufficient
Parameters:
-----------
Q : float
Fixed order quantity
"""
orders = np.zeros(self.T)
inventory = np.zeros(self.T + 1)
setup_costs = np.zeros(self.T)
holding_costs = np.zeros(self.T)
for t in range(self.T):
# Check if order needed
if inventory[t] < self.demands[t]:
orders[t] = Q
inventory[t] += Q
# Satisfy demand
inventory[t] -= self.demands[t]
# Handle backorders if negative
if inventory[t] < 0:
# Need more orders
num_orders = int(np.ceil(-inventory[t] / Q))
orders[t] += num_orders * Q
inventory[t] += num_orders * Q
# Costs
if orders[t] > 0:
setup_costs[t] = self.S * (orders[t] / Q) # Proportional setups
holding_costs[t] = max(0, inventory[t]) * self.h
# Carry forward
inventory[t + 1] = inventory[t]
total_setup = setup_costs.sum()
total_holding = holding_costs.sum()
total_cost = total_setup + total_holding
return {
'method': f'FOQ (Q={Q})',
'orders': orders,
'inventory': inventory[:-1],
'setup_cost': total_setup,
'holding_cost': total_holding,
'total_cost': total_cost,
'num_orders': (orders > 0).sum()
}
def silver_meal(self) -> Dict:
"""
Silver-Meal Heuristic
Iteratively add periods to lot, stop when average cost per period increases
"""
orders = np.zeros(self.T)
inventory = np.zeros(self.T + 1)
t = 0
while t < self.T:
# Determine lot size starting at period t
best_periods = 1
min_avg_cost = float('inf')
for k in range(1, self.T - t + 1):
# Consider covering periods t through t+k-1
# Cost = Setup cost + holding cost for carrying inventory
total_demand = sum(self.demands[t:t+k])
holding_cost = sum((j - t) * self.demands[t+j] * self.h
for j in range(k))
total_cost = self.S + holding_cost
avg_cost = total_cost / k
if avg_cost < min_avg_cost:
min_avg_cost = avg_cost
best_periods = k
else:
# Average cost increased, stop
break
# Place order for best_periods worth of demand
order_qty = sum(self.demands[t:t+best_periods])
orders[t] = order_qty
# Update inventory levels
inv = order_qty
for j in range(best_periods):
if t + j < self.T:
inventory[t + j] = inv
inv -= self.demands[t + j]
t += best_periods
# Calculate costs
setup_costs = np.where(orders > 0, self.S, 0)
holding_costs = inventory[:-1] * self.h
return {
'method': 'Silver-Meal',
'orders': orders,
'inventory': inventory[:-1],
'setup_cost': setup_costs.sum(),
'holding_cost': holding_costs.sum(),
'total_cost': setup_costs.sum() + holding_costs.sum(),
'num_orders': (orders > 0).sum()
}
def least_unit_cost(self) -> Dict:
"""
Least Unit Cost (LUC) Heuristic
For each order, add periods until cost per unit increases
"""
orders = np.zeros(self.T)
inventory = np.zeros(self.T + 1)
t = 0
while t < self.T:
min_unit_cost = float('inf')
best_periods = 1
for k in range(1, self.T - t + 1):
# Total demand covered
total_demand = sum(self.demands[t:t+k])
# Holding cost
holding_cost = sum((j - t) * self.demands[t+j] * self.h
for j in range(k))
# Total cost
total_cost = self.S + holding_cost
# Unit cost
unit_cost = total_cost / total_demand
if unit_cost < min_unit_cost:
min_unit_cost = unit_cost
best_periods = k
else:
break
# Place order
order_qty = sum(self.demands[t:t+best_periods])
orders[t] = order_qty
# Update inventory
inv = order_qty
for j in range(best_periods):
if t + j < self.T:
inventory[t + j] = inv
inv -= self.demands[t + j]
t += best_periods
# Calculate costs
setup_costs = np.where(orders > 0, self.S, 0)
holding_costs = inventory[:-1] * self.h
return {
'method': 'Least Unit Cost',
'orders': orders,
'inventory': inventory[:-1],
'setup_cost': setup_costs.sum(),
'holding_cost': holding_costs.sum(),
'total_cost': setup_costs.sum() + holding_costs.sum(),
'num_orders': (orders > 0).sum()
}
def wagner_whitin(self) -> Dict:
"""
Wagner-Whitin Algorithm
Dynamic programming approach - finds optimal solution
for uncapacitated lot-sizing problem
Time complexity: O(T²)
"""
T = self.T
# F[t] = minimum cost for periods 1..t
F = np.full(T + 1, np.inf)
F[0] = 0
# Predecessor: which period did we last order from?
pred = np.zeros(T + 1, dtype=int)
# DP forward pass
for t in range(1, T + 1):
for j in range(t):
# Consider ordering in period j to cover demand through period t
# Cost = F[j] + setup cost + holding cost
# Holding cost for covering periods j+1 through t
holding_cost = 0
for k in range(j + 1, t + 1):
# Hold demand[k-1] for (k - j - 1) periods
holding_cost += (k - j - 1) * self.demands[k - 1] * self.h
cost = F[j] + self.S + holding_cost
if cost < F[t]:
F[t] = cost
pred[t] = j
# Backtrack to find order periods
orders = np.zeros(T)
t = T
while t > 0:
j = pred[t]
# Order in period j to cover through period t
order_qty = sum(self.demands[j:t])
orders[j] = order_qty
t = j
# Reconstruct inventory
inventory = np.zeros(T + 1)
for t in range(T):
inventory[t] += orders[t]
inventory[t] -= self.demands[t]
inventory[t + 1] = inventory[t]
# Calculate costs
setup_costs = np.where(orders > 0, self.S, 0)
holding_costs = inventory[:-1] * self.h
return {
'method': 'Wagner-Whitin (Optimal)',
'orders': orders,
'inventory': inventory[:-1],
'setup_cost': setup_costs.sum(),
'holding_cost': holding_costs.sum(),
'total_cost': setup_costs.sum() + holding_costs.sum(),
'num_orders': (orders > 0).sum(),
'optimal': True
}
def compare_methods(self) -> pd.DataFrame:
"""Compare all lot-sizing methods"""
methods = [
self.lot_for_lot(),
self.silver_meal(),
self.least_unit_cost(),
self.wagner_whitin()
]
# Add FOQ with EOQ-based quantity
avg_demand = self.demands.mean()
if avg_demand > 0:
eoq = np.sqrt(2 * avg_demand * self.T * self.S / self.h)
methods.append(self.fixed_order_quantity(eoq))
results = []
for method in methods:
results.append({
'Method': method['method'],
'Total Cost': method['total_cost'],
'Setup Cost': method['setup_cost'],
'Holding Cost': method['holding_cost'],
'Num Orders': method['num_orders'],
'Avg Inventory': method['inventory'].mean()
})
df = pd.DataFrame(results)
df = df.sort_values('Total Cost')
return df
def plot_solution(self, solution: Dict):
"""Visualize lot-sizing solution"""
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(14, 10))
periods = np.arange(1, self.T + 1)
# Plot 1: Demand and Orders
ax1.bar(periods, self.demands, alpha=0.6, label='Demand', color='blue')
order_periods = periods[solution['orders'] > 0]
order_qtys = solution['orders'][solution['orders'] > 0]
ax1.bar(order_periods, order_qtys, alpha=0.8, label='Orders', color='green')
ax1.set_xlabel('Period', fontsize=11)
ax1.set_ylabel('Quantity', fontsize=11)
ax1.set_title(f"{solution['method']}: Demand and Order Pattern",
fontsize=12, fontweight='bold')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Plot 2: Inventory Levels
ax2.plot(periods, solution['inventory'], marker='o', linewidth=2,
color='orange', label='Ending Inventory')
ax2.fill_between(periods, 0, solution['inventory'], alpha=0.3, color='orange')
ax2.axhline(y=solution['inventory'].mean(), linestyle='--',
color='red', label=f"Avg Inv = {solution['inventory'].mean():.1f}")
ax2.set_xlabel('Period', fontsize=11)
ax2.set_ylabel('Inventory Level', fontsize=11)
ax2.set_title('Inventory Over Time', fontsize=12, fontweight='bold')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Plot 3: Costs
setup_costs = np.where(solution['orders'] > 0, self.S, 0)
holding_costs = solution['inventory'] * self.h
width = 0.35
ax3.bar(periods - width/2, setup_costs, width, label='Setup Cost',
color='red', alpha=0.7)
ax3.bar(periods + width/2, holding_costs, width, label='Holding Cost',
color='blue', alpha=0.7)
ax3.set_xlabel('Period', fontsize=11)
ax3.set_ylabel('Cost ($)', fontsize=11)
ax3.set_title('Period Costs', fontsize=12, fontweight='bold')
ax3.legend()
ax3.grid(True, alpha=0.3)
plt.tight_layout()
return plt
# Example Usage
def example_lot_sizing():
"""Example: Multi-period lot-sizing problem"""
print("\n" + "=" * 70)
print("MULTI-PERIOD LOT-SIZING PROBLEM")
print("=" * 70)
# 12-period planning horizon with varying demand
demands = [50, 60, 40, 80, 100, 90, 70, 60, 50, 80, 90, 100]
problem = LotSizingProblem(
demands=demands,
setup_cost=200, # $200 per order
holding_cost=2, # $2 per unit per period
unit_cost=10 # $10 per unit (often ignored)
)
print("\nProblem Data:")
print(f" Planning Horizon: {problem.T} periods")
print(f" Setup Cost: ${problem.S}")
print(f" Holding Cost: ${problem.h}/unit/period")
print(f" Total Demand: {problem.demands.sum():.0f} units")
print(f" Average Demand: {problem.demands.mean():.1f} units/period")
print("\n Period-by-period demand:")
for t, d in enumerate(demands, 1):
print(f" Period {t:2d}: {d:3.0f} units")
# Compare all methods
print("\n" + "=" * 70)
print("COMPARISON OF LOT-SIZING METHODS")
print("=" * 70)
comparison = problem.compare_methods()
print("\n" + comparison.to_string(index=False))
# Analyze optimal solution
optimal = problem.wagner_whitin()
print("\n" + "=" * 70)
print(f"OPTIMAL SOLUTION: {optimal['method']}")
print("=" * 70)
print(f"\n{'Total Cost:':<25} ${optimal['total_cost']:,.2f}")
print(f"{'Setup Cost:':<25} ${optimal['setup_cost']:,.2f}")
print(f"{'Holding Cost:':<25} ${optimal['holding_cost']:,.2f}")
print(f"{'Number of Orders:':<25} {optimal['num_orders']}")
print(f"{'Average Inventory:':<25} {optimal['inventory'].mean():.1f} units")
print("\n Order Schedule:")
for t in range(problem.T):
if optimal['orders'][t] > 0:
print(f" Period {t+1}: Order {optimal['orders'][t]:.0f} units")
# Visualize
problem.plot_solution(optimal)
plt.savefig('/tmp/lot_sizing_optimal.png', dpi=300, bbox_inches='tight')
print(f"\nOptimal solution plot saved to /tmp/lot_sizing_optimal.png")
return problem, optimal
if __name__ == "__main__":
example_lot_sizing()
容量制約付きロットサイジング
単一品目の容量制約付きロットサイジング問題(CLSP)
from pulp import *
class CapacitatedLotSizing:
"""
Capacitated Lot-Sizing Problem (CLSP)
Production capacity constraints in each period
"""
def __init__(self, demands: List[float], setup_cost: float,
holding_cost: float, production_cost: float,
capacity: List[float]):
"""
Parameters:
-----------
demands : list
Demand for each period
setup_cost : float
Fixed setup cost per period
holding_cost : float
Holding cost per unit per period
production_cost : float
Variable production cost per unit
capacity : list
Production capacity each period
"""
self.demands = np.array(demands)
self.T = len(demands)
self.S = setup_cost
self.h = holding_cost
self.c = production_cost
self.capacity = np.array(capacity)
def solve_mip(self) -> Dict:
"""
Solve using Mixed-Integer Programming
Decision variables:
- x_t: production quantity in period t
- y_t: binary, 1 if production occurs in period t
- I_t: inventory at end of period t
"""
# Create problem
prob = LpProblem("Capacitated_Lot_Sizing", LpMinimize)
# Decision variables
x = [LpVariable(f"x_{t}", lowBound=0) for t in range(self.T)]
y = [LpVariable(f"y_{t}", cat='Binary') for t in range(self.T)]
I = [LpVariable(f"I_{t}", lowBound=0) for t in range(self.T + 1)]
# Objective: minimize total cost
prob += (lpSum([self.S * y[t] + self.c * x[t] + self.h * I[t]
for t in range(self.T)]))
# Constraints
# Initial inventory
prob += I[0] == 0
# Inventory balance
for t in range(self.T):
prob += I[t + 1] == I[t] + x[t] - self.demands[t]
# Production capacity
for t in range(self.T):
prob += x[t] <= self.capacity[t] * y[t]
# Solve
prob.solve(PULP_CBC_CMD(msg=0))
# Extract solution
production = np.array([x[t].varValue for t in range(self.T)])
setup_decisions = np.array([y[t].varValue for t in range(self.T)])
inventory = np.array([I[t].varValue for t in range(self.T + 1)])
# Calculate costs
setup_cost = self.S * setup_decisions.sum()
prod_cost = self.c * production.sum()
holding_cost = self.h * inventory[:-1].sum()
total_cost = value(prob.objective)
return {
'status': LpStatus[prob.status],
'production': production,
'inventory': inventory[:-1],
'setups': setup_decisions,
'total_cost': total_cost,
'setup_cost': setup_cost,
'production_cost': prod_cost,
'holding_cost': holding_cost,
'num_setups': int(setup_decisions.sum())
}
# Example: Capacitated Lot-Sizing
def example_capacitated():
"""Example: Lot-sizing with capacity constraints"""
print("\n" + "=" * 70)
print("CAPACITATED LOT-SIZING PROBLEM")
print("=" * 70)
demands = [40, 60, 80, 50, 70, 90]
capacity = [100, 100, 100, 100, 100, 100] # Max production per period
problem = CapacitatedLotSizing(
demands=demands,
setup_cost=300,
holding_cost=2,
production_cost=10,
capacity=capacity
)
print("\nProblem Data:")
print(f" Periods: {problem.T}")
print(f" Setup Cost: ${problem.S}")
print(f" Holding Cost: ${problem.h}/unit/period")
print(f" Production Cost: ${problem.c}/unit")
print(f" Capacity per Period: {capacity[0]} units")
print("\n Period Demands:")
for t, d in enumerate(demands, 1):
print(f" Period {t}: {d} units")
# Solve
solution = problem.solve_mip()
print(f"\n{'=' * 70}")
print(f"OPTIMAL SOLUTION")
print("=" * 70)
print(f"\n{'Status:':<25} {solution['status']}")
print(f"{'Total Cost:':<25} ${solution['total_cost']:,.2f}")
print(f"{'Setup Cost:':<25} ${solution['setup_cost']:,.2f}")
print(f"{'Production Cost:':<25} ${solution['production_cost']:,.2f}")
print(f"{'Holding Cost:':<25} ${solution['holding_cost']:,.2f}")
print(f"{'Number of Setups:':<25} {solution['num_setups']}")
print("\n Production Schedule:")
for t in range(problem.T):
if solution['production'][t] > 0.01:
print(f" Period {t+1}: Produce {solution['production'][t]:.0f} units, "
f"Ending Inv: {solution['inventory'][t]:.0f}")
return problem, solution
if __name__ == "__main__":
example_capacitated()
ツールとライブラリ
Python ライブラリ
最適化:
pulp:線形計画法/混合整数計画法pyomo:最適化モデリングscipy.optimize:一般的な最適化ortools:Google OR-Tools
数値計算:
numpy、pandas:データ操作
商用ソフトウェア
生産計画:
- SAP APO:ロットサイジング機能を備えた高度な計画・最適化
- Oracle Demantra:需要・供給計画
- Blue Yonder:サプライチェーン計画
- Kinaxis RapidResponse:統合計画
MRP システム:
- ほとんどの ERP システムにはロットサイジングルールが用意されています(SAP、Oracle、Microsoft Dynamics)
よくある課題と解決策
課題:生産能力の制約
問題:
- 一部の期間で生産能力が不足しています
- 必要なときに生産できません
解決策:
- 生産能力制約付きロットサイズ決定 MIP モデルを使用します
- 残業による生産(コスト増)を検討します
- 需要が少ない期間に事前に在庫を積み増します
- 需要のピーク期間は生産を外部委託します
課題:段取り時間と段取りコスト
問題:
- 段取りには時間(生産能力)とコストの両方がかかります
- 単純なモデルではコストのみを考慮します
解決策:
- 生産能力制約に段取り時間を含めます
- 段取り時間を考慮した CLSP を使用します
- 順序依存の段取り時間には、より複雑なモデルが必要です
課題:複数品目のロットサイズ決定
問題:
- 複数の製品が同じ生産能力を共有します
- 段取りコストまたは段取り時間が共通します
解決策:
- 複数品目の CLSP 定式化を使用します
- 比例ロットサイズ決定(PROPLS)ヒューリスティックを使用します
- 優先順位に基づいて割り当てます
- コード内の複数品目の例を参照してください
課題:需要の不確実性
問題:
- ロットサイズ決定では需要が確定していると仮定します
- 実際の需要は不確実です
解決策:
- ローリングホライズン計画を使用します(各期間に再計画します)
- 需要に安全在庫を加えます
- 需要シナリオを用いたロバスト最適化を行います
- stochastic-inventory-models と dynamic-lot-sizing を参照してください
関連スキル
- economic-order-quantity:単一期間のロットサイズ決定
- dynamic-lot-sizing:時間変動パラメーターと確率的需要
- stochastic-inventory-models:確率的在庫モデル
- master-production-scheduling:総合生産計画
- capacity-planning:長期的な生産能力の意思決定
- production-scheduling:短期的なスケジューリング
コメント
Markdown 対応