omnichannel-fulfillment について
店舗、オンライン、配送センター、サプライヤーを活用する小売業向けの、オムニチャネル配送戦略と実行ガイドです。現状評価から注文ルーティング、在庫可視化、運用改善まで扱います。
主な機能
- 01チャネル構成と配送能力の評価
- 026種類の配送モデルの比較
- 03注文ルーティングと在庫管理の指針
- 04コストと運用上の課題の分析
活用シーン
- 01オンライン注文と店舗受取の計画
- 02店舗発送と配送センターの調達最適化
- 03在庫精度と注文配送の改善
- 04オムニチャネルのコストとサービス水準の評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill omnichannel-fulfillmentインストール後、Claude Code で/omnichannel-fulfillmentを入力して読み込まれたか確認します。
詳細
オムニチャネル・フルフィルメント
あなたは、オムニチャネル・フルフィルメント戦略と実行の専門家です。目標は、あらゆる在庫拠点(店舗、DC、サプライヤー)を活用し、すべてのチャネル(店舗、オンライン、モバイル)で小売業者がシームレスに注文をフルフィルメントできるよう支援し、売上を最大化し、コストを最小限に抑え、卓越した顧客体験を提供することです。
初期評価
オムニチャネル・フルフィルメントを設計する前に、以下を把握してください。
-
チャネル構成
- どのような販売チャネルがありますか?(店舗、ウェブサイト、モバイルアプリ、マーケットプレイス)
- チャネル別の現在の売上構成はどうなっていますか?
- 成長しているチャネルと縮小しているチャネルはどれですか?
- チャネルをまたいだ顧客行動はありますか?(オンラインで調べて、店舗で購入するなど)
-
フルフィルメント能力
- 店舗でオンライン注文をフルフィルメントできますか?(店舗発送機能)
- BOPIS/カーブサイド受け取りに対応していますか?
- 店舗在庫をオンラインで確認できますか?
- DCネットワークの規模と拠点はどうなっていますか?
- 注文管理システム(OMS)は導入されていますか?
-
現在のパフォーマンス
- オンライン注文の配送所要日数はどのくらいですか?(2日、3日、1週間以上など)
- BOPISの利用率はどのくらいですか?
- オンライン注文のうち、店舗発送が占める割合はどのくらいですか?
- 分割配送率はどのくらいですか?
- チャネル別の注文あたりのフルフィルメントコストはいくらですか?
-
ビジネス目標
- 優先事項は、スピード、コスト、顧客体験、それともバランスですか?
- 店舗発送の目標比率はどのくらいですか?
- 在庫回転率の目標はありますか?
- 顧客体験に関する優先事項は何ですか?
- チャネル別の収益性はどうなっていますか?
オムニチャネル・フルフィルメントのフレームワーク
主なフルフィルメントモデル
1. オンラインで購入し、店舗で受け取る(BOPIS)
- 顧客がオンラインで注文し、店舗で受け取ります
- メリット:迅速(当日受け取り)、低コスト、店舗への集客につながります
- 課題:在庫精度、ピッキング効率、顧客の待ち時間
2. 店舗から発送(SFS)
- 店舗が小規模なフルフィルメントセンターとして機能します
- メリット:配送の迅速化、DCの負荷軽減、店舗在庫の活用
- 課題:店舗スタッフの作業負荷、梱包資材、店舗の通常業務との競合
3. DCから発送
- 従来型の集中フルフィルメントです
- メリット:効率的なピッキング、単位あたりのコスト低減、在庫の集約
- 課題:配送所要時間の長期化、輸送コストの増加
4. マーケットプレイス/ドロップシップ
- サプライヤーが顧客に直接発送します
- メリット:在庫への投資が不要、品ぞろえの拡充
- 課題:品質管理、配送時間のばらつき、顧客体験
5. エンドレスアイル
- 店舗で、在庫切れの商品を顧客に代わって注文します
- メリット:販売機会の損失を減らし、顧客体験を向上させます
- 課題:利益率への圧力、運用の複雑さ
6. オンラインで取り置きし、店舗で試着
- 顧客がオンラインで商品を取り置きし、購入前に店舗で試します
- メリット:返品の削減、店舗への集客
- 課題:在庫の確保、運用の複雑さ
オムニチャネル・フルフィルメントの最適化
注文のルーティングと調達ロジック
インテリジェントな注文ルーティング:
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from typing import List, Dict, Tuple
class OmnichannelOrderRouter:
"""
Intelligent order routing for omnichannel fulfillment
Routes orders to optimal fulfillment location based on:
- Inventory availability
- Customer proximity
- Delivery speed
- Fulfillment cost
- Store/DC capacity
"""
def __init__(self, fulfillment_nodes, shipping_matrix, cost_matrix):
"""
Parameters:
- fulfillment_nodes: DataFrame with node info (stores, DCs)
columns: ['node_id', 'type', 'lat', 'lon', 'capacity', 'inventory']
- shipping_matrix: DataFrame with shipping times/costs
- cost_matrix: Dict with cost per unit by fulfillment type
"""
self.nodes = fulfillment_nodes
self.shipping = shipping_matrix
self.costs = cost_matrix
def calculate_fulfillment_score(self, order, node):
"""
Score a fulfillment node for an order
Lower score = better option
Balances speed, cost, and inventory health
"""
# Check inventory availability
if node['inventory'] < order['quantity']:
return float('inf') # Cannot fulfill
# Calculate distance/delivery time
distance = self._calculate_distance(
order['customer_lat'], order['customer_lon'],
node['lat'], node['lon']
)
# Delivery speed score (0-100)
if node['type'] == 'store' and order['method'] == 'BOPIS':
delivery_days = 0 # Same day pickup
elif node['type'] == 'store':
delivery_days = 1 if distance < 50 else 2
else: # DC
delivery_days = 2 if distance < 500 else 3
speed_score = delivery_days * 10
# Cost score (0-100)
if node['type'] == 'store' and order['method'] == 'BOPIS':
fulfillment_cost = self.costs['bopis']
elif node['type'] == 'store':
fulfillment_cost = self.costs['ship_from_store'] + (distance * 0.5)
else:
fulfillment_cost = self.costs['ship_from_dc'] + (distance * 0.3)
cost_score = fulfillment_cost
# Inventory health score (0-100)
# Prefer fulfilling from locations with excess inventory
weeks_of_supply = node['inventory'] / (node['sales_per_week'] + 0.1)
if weeks_of_supply > 8:
inventory_score = -20 # Incentivize using excess inventory
elif weeks_of_supply < 2:
inventory_score = 50 # Penalize low inventory
else:
inventory_score = 0
# Capacity score
capacity_utilization = node['orders_today'] / node['capacity']
if capacity_utilization > 0.9:
capacity_score = 30 # Penalize overloaded nodes
else:
capacity_score = 0
# Weighted total score
total_score = (
speed_score * 0.4 +
cost_score * 0.3 +
inventory_score * 0.2 +
capacity_score * 0.1
)
return total_score
def route_order(self, order):
"""
Route order to optimal fulfillment location
Returns: Best fulfillment node and score
"""
# Score all eligible nodes
scores = []
for idx, node in self.nodes.iterrows():
score = self.calculate_fulfillment_score(order, node)
scores.append({
'node_id': node['node_id'],
'node_type': node['type'],
'score': score,
'delivery_cost': self._estimate_cost(order, node),
'delivery_days': self._estimate_days(order, node)
})
# Select best option
scores_df = pd.DataFrame(scores)
best_option = scores_df.loc[scores_df['score'].idxmin()]
return best_option
def route_multi_item_order(self, order_items, allow_split=True):
"""
Route multi-item order
Decide whether to split shipment or fulfill from single location
"""
# Try single location fulfillment first
single_location_options = []
for idx, node in self.nodes.iterrows():
can_fulfill_all = all(
node['inventory_by_sku'].get(item['sku'], 0) >= item['quantity']
for item in order_items
)
if can_fulfill_all:
total_score = sum(
self.calculate_fulfillment_score(
{'sku': item['sku'], 'quantity': item['quantity'],
'customer_lat': order_items[0]['customer_lat'],
'customer_lon': order_items[0]['customer_lon'],
'method': order_items[0]['method']},
node
)
for item in order_items
)
single_location_options.append({
'node_id': node['node_id'],
'total_score': total_score,
'split': False
})
# Try split shipment if allowed
split_options = []
if allow_split:
item_routings = []
for item in order_items:
best_node = self.route_order(item)
item_routings.append(best_node)
# Calculate split shipment penalty
unique_nodes = len(set(r['node_id'] for r in item_routings))
split_penalty = (unique_nodes - 1) * 15 # Penalize splits
total_split_score = sum(r['score'] for r in item_routings) + split_penalty
split_options.append({
'routings': item_routings,
'total_score': total_split_score,
'split': True,
'num_shipments': unique_nodes
})
# Compare single vs. split
all_options = single_location_options + split_options
if not all_options:
return {'error': 'Cannot fulfill order'}
best_option = min(all_options, key=lambda x: x['total_score'])
return best_option
def _calculate_distance(self, lat1, lon1, lat2, lon2):
"""Calculate distance between two points (miles)"""
from math import radians, sin, cos, sqrt, atan2
R = 3959 # Earth radius in miles
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * atan2(sqrt(a), sqrt(1-a))
return R * c
def _estimate_cost(self, order, node):
"""Estimate fulfillment cost"""
if node['type'] == 'store' and order.get('method') == 'BOPIS':
return self.costs['bopis']
elif node['type'] == 'store':
return self.costs['ship_from_store']
else:
return self.costs['ship_from_dc']
def _estimate_days(self, order, node):
"""Estimate delivery days"""
distance = self._calculate_distance(
order['customer_lat'], order['customer_lon'],
node['lat'], node['lon']
)
if node['type'] == 'store' and order.get('method') == 'BOPIS':
return 0
elif distance < 50:
return 1
elif distance < 200:
return 2
else:
return 3
# Example usage
fulfillment_nodes = pd.DataFrame({
'node_id': ['DC1', 'Store_101', 'Store_102', 'Store_103'],
'type': ['dc', 'store', 'store', 'store'],
'lat': [40.7128, 40.7580, 40.6782, 40.7489],
'lon': [-74.0060, -73.9855, -73.9442, -73.9680],
'capacity': [5000, 50, 50, 50],
'inventory': [10000, 500, 300, 450],
'sales_per_week': [2000, 100, 80, 90],
'orders_today': [2500, 25, 30, 20]
})
cost_matrix = {
'bopis': 2.50,
'ship_from_store': 8.50,
'ship_from_dc': 6.00
}
router = OmnichannelOrderRouter(fulfillment_nodes, None, cost_matrix)
# Route single order
order = {
'sku': 'SKU123',
'quantity': 2,
'customer_lat': 40.7589,
'customer_lon': -73.9851,
'method': 'ship'
}
best_fulfillment = router.route_order(order)
print(f"Route order to: {best_fulfillment['node_id']}")
print(f"Estimated delivery: {best_fulfillment['delivery_days']} days")
print(f"Estimated cost: ${best_fulfillment['delivery_cost']:.2f}")
BOPISの最適化
店舗受け取り業務:
class BOPISOptimizer:
"""
Optimize Buy Online Pickup In Store (BOPIS) operations
Focus on:
- Inventory allocation (reserve for online vs. in-store)
- Picking efficiency
- Customer wait time reduction
- Store capacity management
"""
def __init__(self, store_config):
self.store = store_config
def allocate_inventory(self, sku, available_qty, online_demand_forecast,
instore_demand_forecast):
"""
Allocate inventory between online and in-store channels
Prevent online channel from selling out store inventory
"""
total_demand = online_demand_forecast + instore_demand_forecast
if available_qty >= total_demand:
# Plenty of inventory - allocate all
return {
'online_allocation': online_demand_forecast,
'instore_allocation': instore_demand_forecast,
'allocation_strategy': 'Full allocation'
}
# Scarce inventory - prioritize based on margin and strategy
online_margin_per_unit = self.store['online_margin']
instore_margin_per_unit = self.store['instore_margin']
# Also consider strategic value (online drives future loyalty)
online_strategic_value = 1.2 # 20% premium for online
instore_strategic_value = 1.0
online_value = online_margin_per_unit * online_strategic_value
instore_value = instore_margin_per_unit * instore_strategic_value
# Allocate proportionally to value
online_pct = online_value / (online_value + instore_value)
online_allocation = min(
int(available_qty * online_pct),
online_demand_forecast
)
instore_allocation = available_qty - online_allocation
return {
'online_allocation': online_allocation,
'instore_allocation': instore_allocation,
'allocation_strategy': 'Value-based allocation',
'online_pct': online_pct
}
def optimize_pickup_scheduling(self, orders, picker_capacity_per_hour=10):
"""
Schedule BOPIS orders for picking
Balance speed (customer satisfaction) with efficiency
"""
orders_df = pd.DataFrame(orders)
orders_df['order_time'] = pd.to_datetime(orders_df['order_time'])
# Categorize by urgency
orders_df['hours_until_pickup'] = (
pd.to_datetime(orders_df['requested_pickup_time']) -
orders_df['order_time']
).dt.total_seconds() / 3600
# Priority scoring
def calculate_priority(row):
# High priority: short time until pickup, VIP customer, large order
urgency_score = 100 / max(row['hours_until_pickup'], 0.5)
vip_score = 20 if row.get('is_vip', False) else 0
size_score = min(row['num_items'] * 2, 20)
return urgency_score + vip_score + size_score
orders_df['priority_score'] = orders_df.apply(calculate_priority, axis=1)
# Sort by priority
orders_df = orders_df.sort_values('priority_score', ascending=False)
# Assign to time slots
orders_df['assigned_pick_time'] = None
current_time = datetime.now()
orders_picked = 0
slot_start = current_time
for idx, order in orders_df.iterrows():
# Estimate pick time for this order
pick_time_minutes = order['num_items'] * 2 # 2 min per item
# Assign to current slot
orders_df.at[idx, 'assigned_pick_time'] = slot_start
orders_df.at[idx, 'estimated_ready_time'] = (
slot_start + timedelta(minutes=pick_time_minutes)
)
# Update slot
orders_picked += 1
if orders_picked >= picker_capacity_per_hour:
slot_start += timedelta(hours=1)
orders_picked = 0
return orders_df[['order_id', 'priority_score', 'assigned_pick_time',
'estimated_ready_time', 'requested_pickup_time']]
def calculate_bopis_roi(self, bopis_orders_per_month, avg_basket_size,
bopis_operating_cost_per_order=3.50):
"""
Calculate ROI of BOPIS program
Benefits:
- Incremental sales from convenience
- Additional impulse purchases in store
- Reduced delivery costs vs. ship to home
- Customer lifetime value increase
"""
# Direct costs
monthly_operating_cost = bopis_orders_per_month * bopis_operating_cost_per_order
# Benefits
# 1. Incremental purchases (customers buy more when picking up)
impulse_purchase_rate = 0.35 # 35% buy additional items
avg_impulse_purchase = 25
impulse_revenue = (
bopis_orders_per_month *
impulse_purchase_rate *
avg_impulse_purchase
)
# 2. Cost savings vs. home delivery
home_delivery_cost = 8.50
cost_savings = bopis_orders_per_month * (home_delivery_cost - bopis_operating_cost_per_order)
# 3. Customer lifetime value increase (satisfaction)
ltv_increase_per_customer = 50
new_customers_per_month = bopis_orders_per_month * 0.3
ltv_benefit = new_customers_per_month * ltv_increase_per_customer
# 4. Inventory turns improvement (use store inventory)
inventory_turn_benefit = bopis_orders_per_month * avg_basket_size * 0.05 # 5% carrying cost saved
total_monthly_benefit = (
impulse_revenue +
cost_savings +
ltv_benefit +
inventory_turn_benefit
)
roi = (total_monthly_benefit - monthly_operating_cost) / monthly_operating_cost
return {
'monthly_cost': monthly_operating_cost,
'monthly_benefit': total_monthly_benefit,
'net_monthly_benefit': total_monthly_benefit - monthly_operating_cost,
'roi': roi,
'impulse_revenue': impulse_revenue,
'cost_savings': cost_savings,
'ltv_benefit': ltv_benefit
}
# Example
bopis_optimizer = BOPISOptimizer({
'online_margin': 15,
'instore_margin': 18
})
# Inventory allocation
allocation = bopis_optimizer.allocate_inventory(
sku='SKU456',
available_qty=50,
online_demand_forecast=30,
instore_demand_forecast=35
)
print(f"Online allocation: {allocation['online_allocation']} units")
print(f"In-store allocation: {allocation['instore_allocation']} units")
# ROI calculation
roi_analysis = bopis_optimizer.calculate_bopis_roi(
bopis_orders_per_month=2500,
avg_basket_size=75
)
print(f"\nBOPIS ROI: {roi_analysis['roi']:.1%}")
print(f"Net monthly benefit: ${roi_analysis['net_monthly_benefit']:,.0f}")
店舗発送の最適化
店舗のフルフィルメント能力:
class ShipFromStoreOptimizer:
"""
Optimize ship-from-store operations
Balance store operations with fulfillment duties
"""
def __init__(self, stores_config):
self.stores = pd.DataFrame(stores_config)
def calculate_store_fulfillment_capacity(self, store_id):
"""
Determine optimal ship-from-store capacity for a store
Based on:
- Store traffic patterns
- Staff availability
- Physical space
- Historical performance
"""
store = self.stores[self.stores['store_id'] == store_id].iloc[0]
# Available hours for fulfillment (avoid peak retail hours)
if store['format'] == 'mall':
fulfillment_hours_per_day = 4 # Morning before crowds
elif store['format'] == 'strip':
fulfillment_hours_per_day = 6
else:
fulfillment_hours_per_day = 8
# Picking rate
items_per_hour = 15
# Staff allocation (% of staff that can be dedicated)
staff_available = store['total_staff'] * 0.3 # 30% for online
# Daily capacity
daily_capacity = (
fulfillment_hours_per_day *
items_per_hour *
staff_available
)
# Adjust for store size and layout
if store['square_feet'] < 5000:
space_factor = 0.7 # Cramped
elif store['square_feet'] > 20000:
space_factor = 1.2 # Ample space
else:
space_factor = 1.0
daily_capacity *= space_factor
return {
'store_id': store_id,
'daily_capacity_orders': int(daily_capacity / 3), # 3 items per order avg
'daily_capacity_units': int(daily_capacity),
'fulfillment_hours': fulfillment_hours_per_day,
'recommended_staff': staff_available
}
def allocate_online_orders_to_stores(self, orders, max_distance_miles=50):
"""
Allocate online orders to stores for fulfillment
Balances proximity, capacity, and inventory
"""
allocations = []
for order in orders:
# Find eligible stores (within range, have inventory, have capacity)
eligible_stores = []
for idx, store in self.stores.iterrows():
# Check distance
distance = self._calculate_distance(
order['customer_lat'], order['customer_lon'],
store['lat'], store['lon']
)
if distance > max_distance_miles:
continue
# Check inventory
has_inventory = all(
store['inventory'].get(item['sku'], 0) >= item['quantity']
for item in order['items']
)
if not has_inventory:
continue
# Check capacity
capacity = self.calculate_store_fulfillment_capacity(store['store_id'])
if store['orders_today'] >= capacity['daily_capacity_orders']:
continue
# Calculate score
score = distance # Lower is better
# Adjust for inventory health
total_inventory_days = sum(
store['inventory'].get(item['sku'], 0) / store['sales_velocity'].get(item['sku'], 1)
for item in order['items']
)
if total_inventory_days > 60:
score *= 0.8 # Prefer stores with excess inventory
eligible_stores.append({
'store_id': store['store_id'],
'distance': distance,
'score': score
})
if eligible_stores:
# Select best store
best_store = min(eligible_stores, key=lambda x: x['score'])
allocations.append({
'order_id': order['order_id'],
'assigned_store': best_store['store_id'],
'distance': best_store['distance'],
'estimated_delivery_days': 1 if best_store['distance'] < 25 else 2
})
else:
# No eligible store - route to DC
allocations.append({
'order_id': order['order_id'],
'assigned_store': 'DC',
'distance': None,
'estimated_delivery_days': 3
})
return pd.DataFrame(allocations)
def calculate_sfs_economics(self, annual_sfs_orders, avg_order_value=65):
"""
Calculate economics of ship-from-store program
Compare to DC fulfillment
"""
# Ship-from-store costs
sfs_labor_per_order = 4.50
sfs_packaging_per_order = 1.20
sfs_shipping_per_order = 7.00 # Lower than DC due to proximity
sfs_overhead_per_order = 1.00
sfs_total_cost_per_order = (
sfs_labor_per_order +
sfs_packaging_per_order +
sfs_shipping_per_order +
sfs_overhead_per_order
)
# DC fulfillment costs
dc_labor_per_order = 3.00 # More efficient
dc_packaging_per_order = 1.00
dc_shipping_per_order = 9.50 # Farther from customer
dc_overhead_per_order = 0.80
dc_total_cost_per_order = (
dc_labor_per_order +
dc_packaging_per_order +
dc_shipping_per_order +
dc_overhead_per_order
)
# Annual comparison
sfs_annual_cost = annual_sfs_orders * sfs_total_cost_per_order
dc_annual_cost = annual_sfs_orders * dc_total_cost_per_order
# Additional benefits of SFS
# 1. Reduced inventory holding (use store stock)
inventory_benefit = annual_sfs_orders * avg_order_value * 0.02 # 2% carrying cost
# 2. Faster delivery (premium pricing potential)
speed_benefit = annual_sfs_orders * 1.50 # $1.50 premium per order
# 3. Reduced stockouts (more inventory nodes)
stockout_reduction_benefit = annual_sfs_orders * avg_order_value * 0.01
total_sfs_benefit = inventory_benefit + speed_benefit + stockout_reduction_benefit
net_sfs_advantage = (dc_annual_cost - sfs_annual_cost) + total_sfs_benefit
return {
'annual_sfs_orders': annual_sfs_orders,
'sfs_cost_per_order': sfs_total_cost_per_order,
'dc_cost_per_order': dc_total_cost_per_order,
'cost_difference_per_order': dc_total_cost_per_order - sfs_total_cost_per_order,
'annual_cost_savings': dc_annual_cost - sfs_annual_cost,
'additional_benefits': total_sfs_benefit,
'net_annual_advantage': net_sfs_advantage,
'roi_vs_dc': net_sfs_advantage / sfs_annual_cost if sfs_annual_cost > 0 else 0
}
def _calculate_distance(self, lat1, lon1, lat2, lon2):
"""Calculate distance in miles"""
from math import radians, sin, cos, sqrt, atan2
R = 3959
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat, dlon = lat2 - lat1, lon2 - lon1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * atan2(sqrt(a), sqrt(1-a))
return R * c
# Example
stores_config = [
{'store_id': 'S001', 'format': 'mall', 'total_staff': 20, 'square_feet': 8000,
'lat': 40.7589, 'lon': -73.9851, 'orders_today': 5},
{'store_id': 'S002', 'format': 'strip', 'total_staff': 15, 'square_feet': 12000,
'lat': 40.7128, 'lon': -74.0060, 'orders_today': 8},
]
sfs_optimizer = ShipFromStoreOptimizer(stores_config)
# Calculate capacity
capacity = sfs_optimizer.calculate_store_fulfillment_capacity('S001')
print(f"Store S001 daily capacity: {capacity['daily_capacity_orders']} orders")
# Economics
economics = sfs_optimizer.calculate_sfs_economics(annual_sfs_orders=50000)
print(f"Ship-from-store saves: ${economics['cost_difference_per_order']:.2f} per order")
print(f"Annual advantage: ${economics['net_annual_advantage']:,.0f}")
在庫の一元可視化
在庫のリアルタイム同期:
class UnifiedInventoryManager:
"""
Manage unified inventory across all channels and nodes
Challenges:
- Real-time sync across systems
- Inventory reservations
- In-transit inventory
- Accuracy issues
"""
def __init__(self):
self.inventory = {} # SKU -> {node -> quantity}
self.reservations = {} # SKU -> {node -> reserved_qty}
def get_available_to_promise(self, sku, node_id=None):
"""
Calculate Available-to-Promise (ATP) inventory
ATP = On-hand - Reserved - Safety stock
"""
if node_id:
nodes = [node_id]
else:
nodes = self.inventory.get(sku, {}).keys()
atp_by_node = {}
for node in nodes:
on_hand = self.inventory.get(sku, {}).get(node, 0)
reserved = self.reservations.get(sku, {}).get(node, 0)
safety_stock = self._get_safety_stock(sku, node)
atp = max(0, on_hand - reserved - safety_stock)
atp_by_node[node] = atp
return atp_by_node
def reserve_inventory(self, order_id, sku, quantity, node_id,
reservation_duration_minutes=15):
"""
Reserve inventory for an order
Prevents overselling while customer completes purchase
"""
atp = self.get_available_to_promise(sku, node_id)
if atp.get(node_id, 0) < quantity:
return {
'success': False,
'reason': 'Insufficient ATP',
'available': atp.get(node_id, 0)
}
# Create reservation
if sku not in self.reservations:
self.reservations[sku] = {}
if node_id not in self.reservations[sku]:
self.reservations[sku][node_id] = 0
self.reservations[sku][node_id] += quantity
# Schedule expiration (would use job scheduler in production)
expiration_time = datetime.now() + timedelta(minutes=reservation_duration_minutes)
return {
'success': True,
'order_id': order_id,
'sku': sku,
'quantity': quantity,
'node_id': node_id,
'expires_at': expiration_time
}
def allocate_inventory_across_channels(self, sku, total_available,
channel_forecasts):
"""
Allocate inventory across channels (online, store, wholesale, etc.)
Balance channel priorities and demand
"""
# Calculate total demand
total_demand = sum(channel_forecasts.values())
if total_available >= total_demand:
# Sufficient inventory - allocate all
return channel_forecasts
# Scarce inventory - prioritize by channel value
channel_priority = {
'online': 1.2, # 20% premium (strategic)
'store': 1.0,
'wholesale': 0.8,
'marketplace': 0.9
}
# Weight demands by priority
weighted_demands = {
channel: demand * channel_priority.get(channel, 1.0)
for channel, demand in channel_forecasts.items()
}
total_weighted = sum(weighted_demands.values())
# Allocate proportionally
allocations = {
channel: int(total_available * (weighted / total_weighted))
for channel, weighted in weighted_demands.items()
}
# Handle rounding (allocate remainder to highest priority)
allocated_sum = sum(allocations.values())
if allocated_sum < total_available:
# Give remainder to highest priority channel
highest_priority_channel = max(
channel_priority.items(),
key=lambda x: x[1]
)[0]
if highest_priority_channel in allocations:
allocations[highest_priority_channel] += (total_available - allocated_sum)
return allocations
def _get_safety_stock(self, sku, node):
"""Get safety stock for SKU at node"""
# Simplified - would calculate based on demand variability
return 10
# Example
inventory_manager = UnifiedInventoryManager()
inventory_manager.inventory = {
'SKU789': {
'DC1': 1000,
'Store_101': 50,
'Store_102': 30
}
}
# Check ATP
atp = inventory_manager.get_available_to_promise('SKU789')
print(f"Available-to-Promise: {atp}")
# Reserve inventory
reservation = inventory_manager.reserve_inventory(
order_id='ORD123',
sku='SKU789',
quantity=2,
node_id='Store_101'
)
print(f"Reservation: {reservation['success']}")
# Allocate across channels
channel_forecasts = {
'online': 500,
'store': 400,
'wholesale': 200
}
allocations = inventory_manager.allocate_inventory_across_channels(
sku='SKU789',
total_available=800,
channel_forecasts=channel_forecasts
)
print(f"Channel allocations: {allocations}")
ツールとライブラリ
Pythonライブラリ
最適化:
pulp,pyomo:注文ルーティングのための数理最適化scipy:最適化アルゴリズムortools:車両ルーティングのためのGoogle OR-Tools
地理空間:
geopy:距離の計算folium:地図の可視化geopandas:地理空間データ分析
データ処理:
pandas:データ操作numpy:数値計算scikit-learn:ゾーン最適化のためのクラスタリング
商用ソフトウェア
注文管理システム(OMS):
- Manhattan Active Omni:エンタープライズ向け OMS
- Fluent Commerce:クラウドネイティブ OMS
- IBM Sterling:注文管理
- Blue Yonder Luminate:オムニチャネルのフルフィルメント
- Salesforce Order Management:クラウド OMS
在庫管理:
- Radial:オムニチャネルの在庫最適化
- CommerceHub:分散型注文管理
- Deposco:在庫・フルフィルメントプラットフォーム
- Brightpearl:小売業務プラットフォーム
店舗システム:
- Shopify POS:オンライン連携機能を備えた販売時点管理システム
- Square:POS とオムニチャネル
- NCR:小売向け POS システム
- Oracle Retail:エンタープライズ向け小売管理
よくある課題と解決策
課題:在庫精度
問題:
- 店舗の在庫情報は不正確なことが多い(在庫減耗、スキャンミス)
- オンラインでは在庫ありと表示されても、店舗には在庫がない
- 顧客の不満、BOPIS 注文の失敗
解決策:
- リアルタイムの在庫追跡に RFID を活用
- 継続記録法による在庫管理システム
- 定期的な循環棚卸し
- オンラインチャネル向けの安全在庫バッファー
- 確率的 ATP(精度を考慮)
- ピッキング時に商品がない場合は顧客に通知
課題:分割配送の判断
問題:
- 顧客は迅速な配送を希望する
- 商品が異なる拠点にある
- 分割配送はコストが増加する一方、配送速度は向上する
解決策:
- 注文ごとの費用対効果分析
- 顧客の希望を反映(選択可能にする)
- 分割の最小しきい値(2 日以上短縮できる場合のみ分割)
- 送料無料のしきい値によるインセンティブ
- カートに配送予定日を表示
- インテリジェントな同梱アルゴリズム
課題:店舗の処理能力の制約
問題:
- ピーク時間帯は店舗が混雑する
- オンライン注文のフルフィルメントが店舗業務と競合する
- スタッフの負担が大きい
解決策:
- 動的な処理能力管理
- 比較的空いている店舗に注文を振り分ける
- ピーク時間帯にオンライン注文のフルフィルメント専任スタッフを配置
- 店舗の近くにマイクロフルフィルメントセンターを設置
- 営業時間外のピッキングプログラム
- BOPIS の時間枠管理
課題:返品の複雑さ
問題:
- オンラインで購入し店舗で返品する(複雑)
- 店舗で購入し郵送で返品する
- 返品後の在庫の再配分
- 返金のタイミングに関する問題
解決策:
- 統合返品処理システム
- 店舗での返品時に即時クレジットを付与
- どの拠点でも返品できるポリシー
- 自動化された再入庫ワークフロー
- 返品詐欺の検出
- チャネル横断の返品分析
課題:チャネル別の収益性
問題:
- 一部のフルフィルメント方法は採算が取れない
- チャネルごとの真の収益性を測定するのが難しい
- 送料無料への期待
解決策:
- チャネル別の総原価計算
- 送料無料の最低注文金額
- BOPIS/受け取りのインセンティブ(割引)
- 配送料定額パスのサブスクリプション
- 配送ゾーン別の料金
- 商品単位の収益性分析
出力形式
オムニチャネルフルフィルメント分析レポート
エグゼクティブサマリー:
- チャネル別の総注文数:オンライン 45%、店舗 40%、BOPIS 15%
- 現在のフルフィルメントコスト:注文あたり $8.20
- 店舗出荷の割合:22%
- 目標:店舗出荷を40%に増やし、コストを $7.00 に削減
チャネル別の実績:
| チャネル | 月間注文数 | 平均購入額 | フルフィルメントコスト | 純利益率 | 前年比成長率 |
|---|---|---|---|---|---|
| Eコマース | 45,000 | $72 | $9.50 | 18% | +35% |
| BOPIS | 15,000 | $68 | $3.50 | 24% | +125% |
| 店舗出荷 | 22,000 | $65 | $8.00 | 19% | +80% |
| 店舗販売 | 40,000 | $58 | $2.00 | 22% | -5% |
フルフィルメントネットワークの稼働状況:
| 拠点タイプ | 拠点数 | 容量稼働率 | 1日あたりの注文数 | 平均距離 | 配達速度 |
|---|---|---|---|---|---|
| 配送センター | 3 | 68% | 1,200 | 450 mi | 3.2 日 |
| 店舗出荷 | 45 | 42% | 800 | 35 mi | 1.8 日 |
| BOPIS 受け取り | 120 | 35% | 500 | 8 mi | 当日 |
注文ルーティング分析:
- 単一拠点でのフルフィルメント:78%
- 分割配送:22%
- 注文あたりの平均分割数:1.3 拠点
- ルーティング効率:82%(最適な拠点にルーティングされた割合)
最適化の機会:
-
店舗出荷を拡大(22% → 40%)
- 店舗50店で新たに店舗出荷を有効化
- 投資額:$250K(ピッキングステーション、トレーニング)
- 期待される効果:注文あたり -$1.20、配達時間 -0.8 日
- 年間削減額:$650K
-
在庫精度を向上(85% → 95%)
- 上位50店舗に RFID を導入
- 投資額:$400K
- 期待される効果:BOPIS の失敗を -5%、BOPIS の利用率を +8%
- 年間削減額:$320K + 売上増加
-
インテリジェントな注文ルーティングの最適化
- ML ベースのルーティングエンジンを導入
- 投資額:$150K
- 期待される効果:ルーティング効率を +15%、注文あたり -$0.40
- 年間削減額:$480K
-
BOPIS を拡大
- BOPIS 対応店舗を30店追加
- 投資額:$180K
- 期待される効果:月間注文数を +10K、衝動買いによる購入額を $25 増加
- 年間効果:$850K
導入ロードマップ:
| 四半期 | 施策 | 投資額 | 年間効果 | 投資回収期間 |
|---|---|---|---|---|
| Q1 | 注文ルーティングの最適化 | $150K | $480K | 3.8 か月 |
| Q2 | BOPIS の拡大(30店舗) | $180K | $850K | 2.5 か月 |
| Q3 | 店舗出荷の拡大(50店舗) | $250K | $650K | 4.6 か月 |
| Q4 | RFID による在庫精度向上 | $400K | $320K | 15 か月 |
期待される成果(1年目):
| 指標 | 現状 | 1年目の目標 | 改善 |
|---|---|---|---|
| 注文あたりのフルフィルメントコスト | $8.20 | $7.00 | -15% |
| 店舗出荷の割合 | 22% | 40% | +18 ポイント |
| 平均配達速度 | 2.8 日 | 2.1 日 | -0.7 日 |
| BOPIS の月間注文数 | 15,000 | 25,000 | +67% |
| 分割配送率 | 22% | 18% | -4 ポイント |
| 在庫精度 | 85% | 95% | +10 ポイント |
質問
さらに詳しい情報が必要な場合:
- どのチャネルで販売していますか?(店舗、オンライン、マーケットプレイス、モバイル)
- 店舗では現在、オンライン注文に対応していますか?(BOPIS、店舗出荷)
- ネットワーク内に店舗と DC はそれぞれいくつありますか?
- 現在のオンライン注文数はどのくらいですか?
- オンライン注文のうち、BOPIS の割合はどのくらいですか?
- オンライン注文の平均配達時間はどのくらいですか?
- チャネルをまたいだリアルタイムの在庫状況を把握できますか?
- どのシステムを使用していますか?(OMS、WMS、POS、Eコマースプラットフォーム)
- 主な課題は何ですか?(コスト、速度、顧客体験、複雑さ)
関連スキル
- ecommerce-fulfillment:純粋なeコマースのフルフィルメント戦略
- retail-allocation:店舗への初期配分
- retail-replenishment:店舗の補充最適化
- last-mile-delivery:最終配送の最適化
- route-optimization:配送ルートの計画
- warehouse-design:DCおよびマイクロフルフィルメントセンターの設計
- network-design:フルフィルメントネットワーク戦略
- inventory-optimization:安全在庫と再発注点
- demand-forecasting:チャネル需要の予測