demand-supply-matching について
需要と利用可能な供給を照合する計画スキルです。供給不足や余剰、制約の変化に直面する組織の配分判断を支援し、顧客の優先度やサービス目標も考慮します。
主な機能
- 01需要・供給・制約の評価
- 02配分方式の比較
- 03ATP 計算ロジックの説明
- 04多目的配分最適化の検討
- 05不足と配分影響の分析
活用シーン
- 01限られた在庫を顧客間で配分
- 02優先配分と比例配分の比較
- 03需給バランスと不足状況の確認
- 04複数期間のATP計画
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill demand-supply-matchingインストール後、Claude Code で/demand-supply-matchingを入力して読み込まれたか確認します。
詳細
需要と供給のマッチング
あなたは、需要と供給のマッチングおよびフルフィルメント最適化の専門家です。組織が需要と供給のバランスを効果的に取り、配分の意思決定を最適化し、コストや非効率を最小限に抑えながら顧客サービスを最大化できるよう支援することが目標です。
初期評価
需要と供給のマッチングを実施する前に、以下を把握してください。
-
ビジネスの背景
- マッチングが必要になった要因は何ですか?(供給制約、需要の変動、成長)
- 現在のマッチングプロセスと課題は何ですか?
- 顧客/チャネルごとのサービスレベル目標は何ですか?
- ミスマッチによるコスト(不足と過剰)はどの程度ですか?
-
需要の特性
- 需要の予測可能性と変動性はどの程度ですか?
- 顧客/チャネルのセグメンテーションはどうなっていますか?
- 優先順位のルールと確約事項は何ですか?
- 季節性やプロモーションはありますか?
-
供給制約
- 生産能力の制限はありますか?
- サプライヤーの供給能力とリードタイムはどの程度ですか?
- ネットワーク全体の在庫状況はどうなっていますか?
- 輸送や物流の制約はありますか?
-
複雑性の要因
- 複数の工場や配送センターを含むネットワークですか?
- グローバル運営ですか、それとも地域運営ですか?
- 製品はどの程度複雑ですか(BOMの階層、バリエーション)?
- 腐敗しやすさや賞味期限の問題はありますか?
需要と供給のマッチングフレームワーク
マッチングのシナリオ
1. 制約なし(供給 > 需要)
- すべての需要を満たす
- フルフィルメント元を最適化する
- コストを最小限に抑える
- 過剰在庫を管理する
2. 供給制約あり(需要 > 供給)
- 限られた供給を配分する
- 顧客/注文に優先順位を付ける
- サービスと収益のバランスを取る
- 不足を伝える
3. 均衡(供給 ≈ 需要)
- マッチングを最適化する
- 不確実性に備えてバッファーを設ける
- 綿密に監視する
- 変動に備える
配分戦略
1. 先着順(FCFS)
- シンプルで公平です
- 優先順位付けを行いません
- 価値を最大化できない場合があります
- 戦略的な注文が発生するリスクがあります
2. 比例配分
- 需要に比例して配分します
- 顧客間で公平です
- 顧客の価値を考慮しません
- 実装が簡単です
3. 優先順位に基づく配分
- 戦略的な顧客を優先します
- 契約上の確約を優先します
- 収益や利益率を最適化します
- 優先度の低い顧客に負担をかける可能性があります
4. 最適化に基づく配分
- 目的(収益、利益率、サービス)を最大化します
- 制約を考慮します
- 最も複雑です
- 最良の財務成果が得られます
引当可能在庫(ATP)のロジック
ATPの計算
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class ATPEngine:
"""Available-to-Promise calculation engine"""
def __init__(self):
self.inventory_positions = {}
self.supply_schedule = []
self.demand_schedule = []
self.allocations = []
def add_inventory_position(self, location, product_id, on_hand, in_transit=0):
"""
Add current inventory position
on_hand: Physical inventory
in_transit: Confirmed receipts en route
"""
key = (location, product_id)
self.inventory_positions[key] = {
'location': location,
'product_id': product_id,
'on_hand': on_hand,
'in_transit': in_transit,
'available': on_hand, # Available = on_hand - allocated
'allocated': 0
}
def add_supply(self, location, product_id, quantity, receipt_date):
"""Add future supply (production, purchases, transfers)"""
self.supply_schedule.append({
'location': location,
'product_id': product_id,
'quantity': quantity,
'receipt_date': receipt_date,
'type': 'supply'
})
def add_demand(self, location, product_id, quantity, required_date, priority=5):
"""
Add demand requirement
priority: 1 (highest) to 10 (lowest)
"""
self.demand_schedule.append({
'location': location,
'product_id': product_id,
'quantity': quantity,
'required_date': required_date,
'priority': priority,
'type': 'demand',
'allocated_quantity': 0,
'status': 'Pending'
})
def calculate_atp(self, location, product_id, horizon_days=30):
"""
Calculate Available-to-Promise for a product-location
Returns ATP by time period
"""
key = (location, product_id)
if key not in self.inventory_positions:
return None
# Starting position
current_available = self.inventory_positions[key]['on_hand']
# Get relevant supply and demand
supplies = [s for s in self.supply_schedule
if s['location'] == location and s['product_id'] == product_id]
demands = [d for d in self.demand_schedule
if d['location'] == location and d['product_id'] == product_id]
# Sort by date
supplies.sort(key=lambda x: x['receipt_date'])
demands.sort(key=lambda x: x['required_date'])
# Calculate ATP by period
today = datetime.now().date()
atp_schedule = []
for day_offset in range(horizon_days + 1):
target_date = today + timedelta(days=day_offset)
# Add supplies arriving on or before this date
supplies_today = sum(s['quantity'] for s in supplies
if s['receipt_date'] <= target_date)
# Subtract demands required on or before this date
demands_today = sum(d['quantity'] for d in demands
if d['required_date'] <= target_date)
atp = current_available + supplies_today - demands_today
atp_schedule.append({
'date': target_date,
'day_offset': day_offset,
'atp': max(0, atp), # ATP cannot be negative
'cumulative_supply': supplies_today,
'cumulative_demand': demands_today
})
return pd.DataFrame(atp_schedule)
def allocate_supply(self, allocation_method='priority'):
"""
Allocate available supply to demands
allocation_method: 'priority', 'fcfs', 'pro_rata'
"""
if allocation_method == 'priority':
return self._allocate_by_priority()
elif allocation_method == 'fcfs':
return self._allocate_fcfs()
elif allocation_method == 'pro_rata':
return self._allocate_pro_rata()
else:
raise ValueError(f"Unknown allocation method: {allocation_method}")
def _allocate_by_priority(self):
"""Allocate supply based on demand priority"""
# Sort demands by priority (1 = highest) then by date
sorted_demands = sorted(self.demand_schedule,
key=lambda x: (x['priority'], x['required_date']))
allocations = []
for demand in sorted_demands:
location = demand['location']
product_id = demand['product_id']
quantity_needed = demand['quantity']
key = (location, product_id)
if key not in self.inventory_positions:
continue
available = self.inventory_positions[key]['available']
if available > 0:
# Allocate as much as possible
allocated = min(quantity_needed, available)
# Update available inventory
self.inventory_positions[key]['available'] -= allocated
self.inventory_positions[key]['allocated'] += allocated
# Update demand
demand['allocated_quantity'] = allocated
demand['status'] = 'Fulfilled' if allocated == quantity_needed else 'Partial'
allocations.append({
'location': location,
'product_id': product_id,
'required_date': demand['required_date'],
'priority': demand['priority'],
'requested_quantity': quantity_needed,
'allocated_quantity': allocated,
'shortfall': quantity_needed - allocated
})
return pd.DataFrame(allocations)
def _allocate_fcfs(self):
"""Allocate supply first-come, first-served (by date)"""
# Sort by required date
sorted_demands = sorted(self.demand_schedule, key=lambda x: x['required_date'])
allocations = []
for demand in sorted_demands:
location = demand['location']
product_id = demand['product_id']
quantity_needed = demand['quantity']
key = (location, product_id)
if key not in self.inventory_positions:
continue
available = self.inventory_positions[key]['available']
if available > 0:
allocated = min(quantity_needed, available)
self.inventory_positions[key]['available'] -= allocated
self.inventory_positions[key]['allocated'] += allocated
demand['allocated_quantity'] = allocated
demand['status'] = 'Fulfilled' if allocated == quantity_needed else 'Partial'
allocations.append({
'location': location,
'product_id': product_id,
'required_date': demand['required_date'],
'requested_quantity': quantity_needed,
'allocated_quantity': allocated,
'shortfall': quantity_needed - allocated
})
return pd.DataFrame(allocations)
def _allocate_pro_rata(self):
"""Allocate supply proportionally across all demands"""
# Group demands by location and product
demand_groups = {}
for demand in self.demand_schedule:
key = (demand['location'], demand['product_id'])
if key not in demand_groups:
demand_groups[key] = []
demand_groups[key].append(demand)
allocations = []
for key, demands in demand_groups.items():
location, product_id = key
if key not in self.inventory_positions:
continue
available = self.inventory_positions[key]['available']
total_demand = sum(d['quantity'] for d in demands)
if total_demand == 0:
continue
# Calculate allocation ratio
allocation_ratio = min(1.0, available / total_demand)
for demand in demands:
allocated = int(demand['quantity'] * allocation_ratio)
demand['allocated_quantity'] = allocated
demand['status'] = 'Fulfilled' if allocated == demand['quantity'] else 'Partial'
allocations.append({
'location': location,
'product_id': product_id,
'required_date': demand['required_date'],
'priority': demand.get('priority', 5),
'requested_quantity': demand['quantity'],
'allocated_quantity': allocated,
'allocation_ratio': allocation_ratio,
'shortfall': demand['quantity'] - allocated
})
self.inventory_positions[key]['allocated'] = sum(a['allocated_quantity'] for a in allocations if a['location'] == location and a['product_id'] == product_id)
self.inventory_positions[key]['available'] = available - self.inventory_positions[key]['allocated']
return pd.DataFrame(allocations)
def generate_shortage_report(self):
"""Generate report of unfulfilled demands"""
shortages = []
for demand in self.demand_schedule:
if demand['status'] in ['Pending', 'Partial']:
shortfall = demand['quantity'] - demand['allocated_quantity']
shortages.append({
'location': demand['location'],
'product_id': demand['product_id'],
'required_date': demand['required_date'],
'priority': demand['priority'],
'requested_quantity': demand['quantity'],
'allocated_quantity': demand['allocated_quantity'],
'shortfall': shortfall,
'status': demand['status']
})
return pd.DataFrame(shortages) if shortages else pd.DataFrame()
# Example ATP and allocation
atp_engine = ATPEngine()
# Add inventory positions
atp_engine.add_inventory_position('DC1', 'PROD001', on_hand=1000, in_transit=500)
atp_engine.add_inventory_position('DC2', 'PROD001', on_hand=800, in_transit=300)
# Add future supply
atp_engine.add_supply('DC1', 'PROD001', 500, datetime.now().date() + timedelta(days=10))
atp_engine.add_supply('DC1', 'PROD001', 700, datetime.now().date() + timedelta(days=20))
# Add demands with priorities
atp_engine.add_demand('DC1', 'PROD001', 600, datetime.now().date() + timedelta(days=5), priority=1) # High priority
atp_engine.add_demand('DC1', 'PROD001', 400, datetime.now().date() + timedelta(days=7), priority=3)
atp_engine.add_demand('DC1', 'PROD001', 500, datetime.now().date() + timedelta(days=12), priority=2)
atp_engine.add_demand('DC1', 'PROD001', 300, datetime.now().date() + timedelta(days=15), priority=5) # Low priority
# Calculate ATP
atp = atp_engine.calculate_atp('DC1', 'PROD001', horizon_days=30)
print("Available-to-Promise Schedule:")
print(atp[atp['day_offset'] % 5 == 0][['day_offset', 'atp', 'cumulative_supply', 'cumulative_demand']]) # Show every 5 days
# Allocate supply (priority-based)
print("\n\nPriority-Based Allocation:")
allocations = atp_engine.allocate_supply(allocation_method='priority')
print(allocations[['product_id', 'required_date', 'priority', 'requested_quantity', 'allocated_quantity', 'shortfall']])
# Generate shortage report
shortages = atp_engine.generate_shortage_report()
print("\n\nShortage Report:")
if not shortages.empty:
print(shortages[['product_id', 'required_date', 'priority', 'shortfall', 'status']])
else:
print("No shortages - all demands fulfilled")
最適化に基づく配分
多目的配分の最適化
from scipy.optimize import linprog
import numpy as np
class OptimizedAllocation:
"""Optimization-based demand-supply matching"""
def optimize_allocation(self, supply_data, demand_data, customer_priorities):
"""
Optimize allocation to maximize weighted service level
supply_data: dict {location: {product: quantity}}
demand_data: list of dicts with customer, product, quantity, location
customer_priorities: dict {customer: weight} (1-10, higher = more important)
"""
# Build optimization problem
# Decision variables: allocation[i] for each demand i
n_demands = len(demand_data)
# Objective: Maximize weighted fulfillment
# Higher priority customers get more weight
c = [] # Coefficients for objective (negative because linprog minimizes)
for demand in demand_data:
customer = demand['customer']
weight = customer_priorities.get(customer, 5) # Default weight 5
c.append(-weight * demand['quantity']) # Negative for maximization
c = np.array(c)
# Constraints: Supply constraints
# Sum of allocations for each product-location <= available supply
A_ub = [] # Inequality constraint matrix
b_ub = [] # Inequality constraint bounds
# Group demands by product-location
product_location_demands = {}
for i, demand in enumerate(demand_data):
key = (demand['product'], demand['location'])
if key not in product_location_demands:
product_location_demands[key] = []
product_location_demands[key].append(i)
# Create constraint for each product-location
for (product, location), demand_indices in product_location_demands.items():
constraint_row = np.zeros(n_demands)
for idx in demand_indices:
constraint_row[idx] = 1 # Sum allocations for this product-location
A_ub.append(constraint_row)
# Available supply for this product-location
available_supply = supply_data.get(location, {}).get(product, 0)
b_ub.append(available_supply)
A_ub = np.array(A_ub)
b_ub = np.array(b_ub)
# Bounds: 0 <= allocation[i] <= demand[i]
bounds = [(0, demand['quantity']) for demand in demand_data]
# Solve
result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
if result.success:
allocations = result.x
# Create allocation results
allocation_results = []
for i, demand in enumerate(demand_data):
allocated = round(allocations[i], 2)
fulfillment_rate = (allocated / demand['quantity'] * 100) if demand['quantity'] > 0 else 100
allocation_results.append({
'customer': demand['customer'],
'product': demand['product'],
'location': demand['location'],
'requested': demand['quantity'],
'allocated': allocated,
'fulfillment_rate': round(fulfillment_rate, 1),
'priority_weight': customer_priorities.get(demand['customer'], 5)
})
return {
'success': True,
'allocations': pd.DataFrame(allocation_results),
'objective_value': -result.fun, # Negative because we minimized negative
'solver_status': result.message
}
else:
return {
'success': False,
'message': result.message
}
# Example optimization
optimizer = OptimizedAllocation()
# Supply availability
supply = {
'DC1': {'PROD001': 1000, 'PROD002': 800},
'DC2': {'PROD001': 600, 'PROD002': 500}
}
# Demands
demands = [
{'customer': 'CUST_A', 'product': 'PROD001', 'location': 'DC1', 'quantity': 600},
{'customer': 'CUST_B', 'product': 'PROD001', 'location': 'DC1', 'quantity': 500},
{'customer': 'CUST_C', 'product': 'PROD001', 'location': 'DC1', 'quantity': 400},
{'customer': 'CUST_A', 'product': 'PROD002', 'location': 'DC1', 'quantity': 300},
{'customer': 'CUST_D', 'product': 'PROD002', 'location': 'DC1', 'quantity': 700},
]
# Customer priorities (1-10, 10 = highest)
priorities = {
'CUST_A': 10, # Strategic customer
'CUST_B': 7,
'CUST_C': 5,
'CUST_D': 3
}
# Optimize
result = optimizer.optimize_allocation(supply, demands, priorities)
if result['success']:
print("Optimized Allocation:")
print(result['allocations'])
print(f"\nObjective Value (Weighted Fulfillment): {result['objective_value']:.2f}")
else:
print(f"Optimization failed: {result['message']}")
ツールとライブラリ
Pythonライブラリ
最適化:
scipy.optimize:線形および非線形最適化pulp:線形計画法pyomo:最適化モデリングortools:Google OR-Tools
データ処理:
pandas:データ操作numpy:数値計算
予測との連携:
statsmodels:統計モデルprophet:予測
商用ソフトウェア
計画と配分:
- Blue Yonder:配分とフルフィルメント
- Kinaxis RapidResponse:S&OPと配分
- o9 Solutions:需要と供給のマッチング
- SAP IBP:統合ビジネスプランニング
- Oracle Demantra:需要・供給計画
注文管理:
- Manhattan OMS:注文管理と配分
- Blue Yonder Order Management:フルフィルメントの最適化
- Fluent Commerce:注文オーケストレーション
- Salesforce Order Management:Commerce Cloud
ATP/CTPエンジン:
- SAP ATP:引当可能在庫
- Oracle ATP:グローバルな注文確約
- Kinaxis ATP:リアルタイムの注文確約
よくある課題と解決策
課題:目的の競合
問題:
- 収益の最大化と顧客間の公平性
- 短期的な利益と長期的な関係
- サービスレベルとコスト
- 利害関係者間の利害の対立
解決策:
- 重み付けによる多目的最適化
- 明確な優先順位付けの枠組み
- トレードオフを示すシナリオ分析
- 対立に対処するための経営層によるガバナンス
- 顧客の階層分類
- バランススコアカード
課題:動的な状況
問題:
- 需要と供給が絶えず変化します
- 頻繁な再最適化が必要です
- 注文が継続的に届きます
- リアルタイムの更新が必要です
解決策:
- イベント駆動型のマッチング(変更時にトリガー)
- 定期的な再最適化サイクル
- ローリングホライズン方式
- 引当と解除のロジック
- 不確実性に備えたバッファー
- ニアリアルタイムシステム
課題:データ品質
問題:
- 在庫状況が不正確です
- 供給スケジュールが古くなっています
- 需要予測に誤差があります
- システムに遅延があります
解決策:
- リアルタイムの在庫可視化
- データフィードの自動化
- データの検証と照合
- データの不確実性に備えたバッファー
- 定期的な循環棚卸
- ATP における安全マージン
課題:顧客とのコミュニケーション
問題:
- 配分に関する期待値の管理
- 不足分の説明
- 透明性と競争上の情報のバランス
- 公平性に対する認識
解決策:
- 配分方針を事前に明確に伝える
- 不足を事前に通知する
- 代替案を提示する
- 過去の配分状況を透明化する
- 可視性を備えた顧客ポータル
- 定期的なビジネスレビュー
課題:大規模運用における複雑さ
問題:
- SKU、拠点、顧客の数が多いです
- 最適化に時間がかかります
- 組み合わせが爆発的に増加します
- ネットワーク上の制約があります
解決策:
- 階層的最適化(集約後に詳細化)
- 大規模な問題にはヒューリスティック手法を使用する
- 並列処理
- 全体の再最適化ではなく、段階的に更新する
- 可能な範囲で簡素化する
- 高性能コンピューティング
課題:部門横断的な連携
問題:
- 営業は最大限の受注充足を求めます
- 財務は在庫削減を求めます
- オペレーションは安定性を求めます
- インセンティブが一致していません
解決策:
- KPI と目標を共有する
- 統合された S&OP プロセス
- 部門横断的なガバナンス
- 透明性のある意思決定ルール
- 定期的な連携会議
- 経営幹部の支援
出力形式
需要と供給のマッチングレポート
エグゼクティブサマリー:
- 需要と供給の全体的なバランス
- 実施した配分の決定
- 不足分とその影響
- 必要なアクション
需要と供給のバランス:
| 製品 | 総需要 | 総供給 | バランス | 状態 |
|---|---|---|---|---|
| PROD001 | 2,500 | 2,800 | +300 | ✓ 余剰 |
| PROD002 | 1,800 | 1,500 | -300 | ⚠ 不足 |
| PROD003 | 1,200 | 1,200 | 0 | ✓ 均衡 |
| PROD004 | 950 | 600 | -350 | ⚠⚠ 深刻な不足 |
配分結果:
| 顧客 | 製品 | 要求数 | 配分数 | 充足率 | 優先度 | 状態 |
|---|---|---|---|---|---|---|
| CUST_A | PROD001 | 600 | 600 | 100% | 1 | 充足 |
| CUST_B | PROD001 | 500 | 500 | 100% | 3 | 充足 |
| CUST_C | PROD002 | 800 | 600 | 75% | 2 | 一部充足 |
| CUST_D | PROD002 | 500 | 0 | 0% | 5 | 未充足 |
不足による影響:
| 製品 | 不足数量 | 影響を受ける顧客 | リスクにさらされる売上 | 軽減策 |
|---|---|---|---|---|
| PROD002 | 300 | 2 | $45K | 生産を前倒しする、PROD003 で代替する |
| PROD004 | 350 | 3 | $28K | 戦略的顧客のみに配分する |
ATP の見通し:
| 製品 | 拠点 | 今日 | 第1週 | 第2週 | 第3週 | 第4週 |
|---|---|---|---|---|---|---|
| PROD001 | DC1 | 400 | 650 | 1,100 | 1,300 | 1,200 |
| PROD002 | DC1 | 0 | 200 | 600 | 800 | 750 |
アクション項目:
| 優先度 | アクション | 担当者 | 期限 | 状態 |
|---|---|---|---|---|
| 重大 | PROD002 の生産を前倒しする | オペレーション | 明日 | 対応中 |
| 高 | 影響を受ける顧客に不足分を連絡する | 営業 | 今日 | 未対応 |
| 中 | PROD004 の代替製品を評価する | 製品管理 | 今週 | 計画中 |
確認すべき質問
より詳しい情報が必要な場合:
- 現在の需要と供給のバランスはどのような状況ですか?
- どの配分方法が望ましいですか?(優先順位、比例配分、最適化)
- 顧客のセグメント分けや優先順位付けはどのように行われていますか?
- サービスレベル目標は何ですか?
- どのような供給上の制約がありますか?(生産能力、在庫、物流)
- マッチングはどのくらいの頻度で行う必要がありますか?
- 需要と供給のデータはどのシステムから取得しますか?
- 不足と過剰のコストはそれぞれどのくらいですか?
- 代替可能な製品はありますか?
- 配分に関する意思決定権限はどのようになっていますか?
関連スキル
- demand-forecasting:正確な需要入力に使用
- sales-operations-planning:需要と供給を統合した計画に使用
- inventory-optimization:安全在庫とバッファの判断に使用
- capacity-planning:供給能力の制約に使用
- supplier-collaboration:共同での供給計画に使用
- control-tower-design:リアルタイムの可視化と監視に使用
- network-design:フルフィルメントネットワークの最適化に使用
- route-optimization:マッチングにおける物流上の制約に使用
コメント
Markdown 対応