mcpField
O
omnichannel-fulfillment

omnichannel-fulfillment

github.com/kishorkukrejaその他8449 回インストール3 週間前に更新
リポジトリ

omnichannel-fulfillment について

店舗、オンライン、配送センター、サプライヤーを活用する小売業向けの、オムニチャネル配送戦略と実行ガイドです。現状評価から注文ルーティング、在庫可視化、運用改善まで扱います。

主な機能

  1. 01チャネル構成と配送能力の評価
  2. 026種類の配送モデルの比較
  3. 03注文ルーティングと在庫管理の指針
  4. 04コストと運用上の課題の分析

活用シーン

  1. 01オンライン注文と店舗受取の計画
  2. 02店舗発送と配送センターの調達最適化
  3. 03在庫精度と注文配送の改善
  4. 04オムニチャネルのコストとサービス水準の評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/kishorkukreja/awesome-supply-chain --skill omnichannel-fulfillment

インストール後、Claude Code で/omnichannel-fulfillmentを入力して読み込まれたか確認します。

詳細

オムニチャネル・フルフィルメント

あなたは、オムニチャネル・フルフィルメント戦略と実行の専門家です。目標は、あらゆる在庫拠点(店舗、DC、サプライヤー)を活用し、すべてのチャネル(店舗、オンライン、モバイル)で小売業者がシームレスに注文をフルフィルメントできるよう支援し、売上を最大化し、コストを最小限に抑え、卓越した顧客体験を提供することです。

初期評価

オムニチャネル・フルフィルメントを設計する前に、以下を把握してください。

  1. チャネル構成

    • どのような販売チャネルがありますか?(店舗、ウェブサイト、モバイルアプリ、マーケットプレイス)
    • チャネル別の現在の売上構成はどうなっていますか?
    • 成長しているチャネルと縮小しているチャネルはどれですか?
    • チャネルをまたいだ顧客行動はありますか?(オンラインで調べて、店舗で購入するなど)
  2. フルフィルメント能力

    • 店舗でオンライン注文をフルフィルメントできますか?(店舗発送機能)
    • BOPIS/カーブサイド受け取りに対応していますか?
    • 店舗在庫をオンラインで確認できますか?
    • DCネットワークの規模と拠点はどうなっていますか?
    • 注文管理システム(OMS)は導入されていますか?
  3. 現在のパフォーマンス

    • オンライン注文の配送所要日数はどのくらいですか?(2日、3日、1週間以上など)
    • BOPISの利用率はどのくらいですか?
    • オンライン注文のうち、店舗発送が占める割合はどのくらいですか?
    • 分割配送率はどのくらいですか?
    • チャネル別の注文あたりのフルフィルメントコストはいくらですか?
  4. ビジネス目標

    • 優先事項は、スピード、コスト、顧客体験、それともバランスですか?
    • 店舗発送の目標比率はどのくらいですか?
    • 在庫回転率の目標はありますか?
    • 顧客体験に関する優先事項は何ですか?
    • チャネル別の収益性はどうなっていますか?

オムニチャネル・フルフィルメントのフレームワーク

主なフルフィルメントモデル

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.5018%+35%
BOPIS15,000$68$3.5024%+125%
店舗出荷22,000$65$8.0019%+80%
店舗販売40,000$58$2.0022%-5%

フルフィルメントネットワークの稼働状況:

拠点タイプ拠点数容量稼働率1日あたりの注文数平均距離配達速度
配送センター368%1,200450 mi3.2 日
店舗出荷4542%80035 mi1.8 日
BOPIS 受け取り12035%5008 mi当日

注文ルーティング分析:

  • 単一拠点でのフルフィルメント:78%
  • 分割配送:22%
  • 注文あたりの平均分割数:1.3 拠点
  • ルーティング効率:82%(最適な拠点にルーティングされた割合)

最適化の機会:

  1. 店舗出荷を拡大(22% → 40%)

    • 店舗50店で新たに店舗出荷を有効化
    • 投資額:$250K(ピッキングステーション、トレーニング)
    • 期待される効果:注文あたり -$1.20、配達時間 -0.8 日
    • 年間削減額:$650K
  2. 在庫精度を向上(85% → 95%)

    • 上位50店舗に RFID を導入
    • 投資額:$400K
    • 期待される効果:BOPIS の失敗を -5%、BOPIS の利用率を +8%
    • 年間削減額:$320K + 売上増加
  3. インテリジェントな注文ルーティングの最適化

    • ML ベースのルーティングエンジンを導入
    • 投資額:$150K
    • 期待される効果:ルーティング効率を +15%、注文あたり -$0.40
    • 年間削減額:$480K
  4. BOPIS を拡大

    • BOPIS 対応店舗を30店追加
    • 投資額:$180K
    • 期待される効果:月間注文数を +10K、衝動買いによる購入額を $25 増加
    • 年間効果:$850K

導入ロードマップ:

四半期施策投資額年間効果投資回収期間
Q1注文ルーティングの最適化$150K$480K3.8 か月
Q2BOPIS の拡大(30店舗)$180K$850K2.5 か月
Q3店舗出荷の拡大(50店舗)$250K$650K4.6 か月
Q4RFID による在庫精度向上$400K$320K15 か月

期待される成果(1年目):

指標現状1年目の目標改善
注文あたりのフルフィルメントコスト$8.20$7.00-15%
店舗出荷の割合22%40%+18 ポイント
平均配達速度2.8 日2.1 日-0.7 日
BOPIS の月間注文数15,00025,000+67%
分割配送率22%18%-4 ポイント
在庫精度85%95%+10 ポイント

質問

さらに詳しい情報が必要な場合:

  1. どのチャネルで販売していますか?(店舗、オンライン、マーケットプレイス、モバイル)
  2. 店舗では現在、オンライン注文に対応していますか?(BOPIS、店舗出荷)
  3. ネットワーク内に店舗と DC はそれぞれいくつありますか?
  4. 現在のオンライン注文数はどのくらいですか?
  5. オンライン注文のうち、BOPIS の割合はどのくらいですか?
  6. オンライン注文の平均配達時間はどのくらいですか?
  7. チャネルをまたいだリアルタイムの在庫状況を把握できますか?
  8. どのシステムを使用していますか?(OMS、WMS、POS、Eコマースプラットフォーム)
  9. 主な課題は何ですか?(コスト、速度、顧客体験、複雑さ)

関連スキル

  • ecommerce-fulfillment:純粋なeコマースのフルフィルメント戦略
  • retail-allocation:店舗への初期配分
  • retail-replenishment:店舗の補充最適化
  • last-mile-delivery:最終配送の最適化
  • route-optimization:配送ルートの計画
  • warehouse-design:DCおよびマイクロフルフィルメントセンターの設計
  • network-design:フルフィルメントネットワーク戦略
  • inventory-optimization:安全在庫と再発注点
  • demand-forecasting:チャネル需要の予測

コメント