mcpField
R
replenishment-strategy

replenishment-strategy

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

replenishment-strategy について

replenishment-strategyは、在庫補充方針を設計するサプライチェーン計画チーム向けのスキルです。方針選定、パラメーター計算、ネットワーク計画、コストの調整を扱います。

主な機能

  1. 01連続・定期・最小最大補充方針
  2. 02需要主導補充とベンダー管理在庫
  3. 03配送所要量計画
  4. 04多段階ネットワークとピック前補充計画
  5. 05補充数量のコスト最適化

活用シーン

  1. 01SKU特性に応じた補充方針の選定
  2. 02配送センターと店舗の在庫計画
  3. 03再発注点と発注量の設定
  4. 04欠品・保有・発注コストの調整

インストール

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

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

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

詳細

補充戦略

在庫補充戦略と需要主導型サプライチェーン計画の専門家として、在庫切れや過剰在庫を最小限に抑えながら、最適な在庫水準を維持する効率的で柔軟な補充システムの設計を支援します。

初期評価

補充戦略を設計する前に、次の点を把握します。

  1. ネットワーク構造

    • サプライチェーンの階層はどのようになっていますか?(サプライヤー → DC → 店舗/顧客)
    • 各階層の拠点数はいくつですか?
    • 在庫を保有する拠点はどこですか?
    • 補充関係はどのようになっていますか?(どの拠点がどの拠点に供給するか)
  2. 需要特性

    • 各階層の需要変動はどの程度ですか?
    • 階層間のリードタイムはどのくらいですか?
    • 注文パターンはどのようなものですか?(安定、不規則、季節性)
    • 各階層の予測精度はどの程度ですか?
  3. 運用上の制約

    • 最小発注数量(MOQ)はありますか?
    • 発注頻度に制限はありますか?(毎日、毎週、毎月)
    • 輸送上の制約はありますか?(トラック満載での輸送が望ましいなど)
    • 保管容量の上限はありますか?
  4. 現状

    • 現在の補充方法は何ですか?
    • 在庫切れの頻度や過剰在庫の問題はありますか?
    • 補充のリードタイムはどのくらいですか?
    • サービスレベルの実績はどの程度ですか?

補充戦略フレームワーク

主な補充方法

1. 連続レビュー方式(s, Q)

  • 在庫を継続的に監視します
  • 在庫が発注点 s に達したら、固定数量 Q を発注します
  • 最適な用途:高額品、自動化システム

2. 定期レビュー方式(R, S)

  • R 期間ごとに在庫を確認します
  • 在庫を S 水準まで補充します
  • 最適な用途:同じサプライヤーから仕入れる複数品目、調整された補充

3. Min-Max方式(s, S)

  • 在庫が最小値(s)以下になったら、最大値(S)まで補充します
  • 連続方式と定期方式を組み合わせた方式です
  • 最適な用途:小売、シンプルなシステム

4. 需要主導型補充(DDR)

  • 実際の消費量/販売量に基づきます
  • プル型で、需要に柔軟に対応します
  • 最適な用途:需要変動が大きい場合、リードタイムが短い場合

5. ベンダー管理在庫(VMI)

  • サプライヤーが在庫水準を管理します
  • サプライヤーが補充を担当します
  • 最適な用途:サプライヤーとの強固な関係、委託在庫

補充ポリシーモデル

連続レビュー方式(s, Q)ポリシー

パラメーター:

  • s(発注点):発注するタイミング
  • Q(発注数量):発注する数量(多くの場合はEOQ)
import numpy as np
from scipy import stats
import pandas as pd

def calculate_reorder_point(demand_avg_daily, lead_time_days,
                           demand_std_daily, service_level=0.95):
    """
    Calculate reorder point for continuous review

    Parameters:
    - demand_avg_daily: Average daily demand
    - lead_time_days: Lead time in days
    - demand_std_daily: Standard deviation of daily demand
    - service_level: Target service level (e.g., 0.95)

    Returns:
    - Reorder point
    """

    # Expected demand during lead time
    expected_demand = demand_avg_daily * lead_time_days

    # Safety stock
    z = stats.norm.ppf(service_level)
    std_during_lt = demand_std_daily * np.sqrt(lead_time_days)
    safety_stock = z * std_during_lt

    # Reorder point
    rop = expected_demand + safety_stock

    return {
        'reorder_point': round(rop, 0),
        'expected_demand_lt': round(expected_demand, 0),
        'safety_stock': round(safety_stock, 0),
        'service_level': service_level
    }

# Example
rop_params = calculate_reorder_point(
    demand_avg_daily=100,
    lead_time_days=14,
    demand_std_daily=20,
    service_level=0.95
)

print(f"Reorder Point: {rop_params['reorder_point']} units")
print(f"  Expected demand during LT: {rop_params['expected_demand_lt']}")
print(f"  Safety stock: {rop_params['safety_stock']}")

シミュレーション:

def simulate_continuous_review(demand_series, lead_time, reorder_point,
                               order_quantity, initial_inventory):
    """
    Simulate continuous review (s, Q) policy

    Parameters:
    - demand_series: Array of daily demand
    - lead_time: Lead time in days
    - reorder_point: Reorder point (s)
    - order_quantity: Order quantity (Q)
    - initial_inventory: Starting inventory

    Returns:
    - Simulation results DataFrame
    """

    results = []
    inventory = initial_inventory
    on_order = []  # Queue of orders in transit

    for day, demand in enumerate(demand_series):
        # Check for order arrivals
        arrived_orders = [o for o in on_order if o['arrival_day'] == day]
        for order in arrived_orders:
            inventory += order['quantity']
            on_order.remove(order)

        # Satisfy demand
        actual_demand = min(demand, inventory)
        stockout = max(0, demand - inventory)
        inventory = max(0, inventory - demand)

        # Check if need to order
        inventory_position = inventory + sum(o['quantity'] for o in on_order)
        order_placed = False

        if inventory_position <= reorder_point:
            on_order.append({
                'quantity': order_quantity,
                'order_day': day,
                'arrival_day': day + lead_time
            })
            order_placed = True

        results.append({
            'day': day,
            'demand': demand,
            'inventory_on_hand': inventory,
            'inventory_position': inventory_position,
            'stockout': stockout,
            'order_placed': order_placed
        })

    return pd.DataFrame(results)

# Example simulation
np.random.seed(42)
demand = np.random.poisson(100, 365)  # 365 days

sim_results = simulate_continuous_review(
    demand_series=demand,
    lead_time=14,
    reorder_point=1600,
    order_quantity=1000,
    initial_inventory=2000
)

# Performance metrics
print(f"Average inventory: {sim_results['inventory_on_hand'].mean():.0f}")
print(f"Stockout days: {(sim_results['stockout'] > 0).sum()}")
print(f"Service level: {(1 - (sim_results['stockout'] > 0).sum() / len(sim_results)):.1%}")
print(f"Orders placed: {sim_results['order_placed'].sum()}")
定期レビュー方式(R, S)ポリシー

パラメーター:

  • R(レビュー期間):確認頻度(例:毎週)
  • S(目標在庫水準):目標とする在庫水準
def calculate_order_up_to_level(demand_avg_daily, review_period_days,
                               lead_time_days, demand_std_daily,
                               service_level=0.95):
    """
    Calculate order-up-to level for periodic review

    Must cover demand during review period + lead time
    """

    # Total coverage period
    coverage_period = review_period_days + lead_time_days

    # Expected demand during coverage period
    expected_demand = demand_avg_daily * coverage_period

    # Safety stock for coverage period
    z = stats.norm.ppf(service_level)
    std_during_period = demand_std_daily * np.sqrt(coverage_period)
    safety_stock = z * std_during_period

    # Order-up-to level
    order_up_to = expected_demand + safety_stock

    return {
        'order_up_to_level': round(order_up_to, 0),
        'expected_demand': round(expected_demand, 0),
        'safety_stock': round(safety_stock, 0),
        'coverage_period_days': coverage_period,
        'service_level': service_level
    }

# Example
periodic_params = calculate_order_up_to_level(
    demand_avg_daily=100,
    review_period_days=7,    # Weekly review
    lead_time_days=14,
    demand_std_daily=20,
    service_level=0.95
)

print(f"Order-up-to Level (S): {periodic_params['order_up_to_level']} units")
print(f"Coverage period: {periodic_params['coverage_period_days']} days")

シミュレーション:

def simulate_periodic_review(demand_series, review_period, order_up_to_level,
                            lead_time, initial_inventory):
    """
    Simulate periodic review (R, S) policy
    """

    results = []
    inventory = initial_inventory
    on_order = []

    for day, demand in enumerate(demand_series):
        # Check for arrivals
        arrived_orders = [o for o in on_order if o['arrival_day'] == day]
        for order in arrived_orders:
            inventory += order['quantity']
            on_order.remove(order)

        # Satisfy demand
        actual_demand = min(demand, inventory)
        stockout = max(0, demand - inventory)
        inventory = max(0, inventory - demand)

        # Check if review day
        is_review_day = (day % review_period == 0)
        order_qty = 0

        if is_review_day:
            inventory_position = inventory + sum(o['quantity'] for o in on_order)
            order_qty = max(0, order_up_to_level - inventory_position)

            if order_qty > 0:
                on_order.append({
                    'quantity': order_qty,
                    'order_day': day,
                    'arrival_day': day + lead_time
                })

        results.append({
            'day': day,
            'demand': demand,
            'inventory_on_hand': inventory,
            'is_review_day': is_review_day,
            'order_quantity': order_qty,
            'stockout': stockout
        })

    return pd.DataFrame(results)

# Example
sim_results = simulate_periodic_review(
    demand_series=demand,
    review_period=7,
    order_up_to_level=2200,
    lead_time=14,
    initial_inventory=2000
)

print(f"Average inventory: {sim_results['inventory_on_hand'].mean():.0f}")
print(f"Stockout days: {(sim_results['stockout'] > 0).sum()}")
print(f"Orders placed: {(sim_results['order_quantity'] > 0).sum()}")
Min-Max補充

シンプルで広く使われている方式:

  • Min:補充のトリガーとなる水準(発注点に相当)
  • Max:目標水準(この水準まで補充します)
def calculate_min_max(demand_avg_daily, lead_time_days, review_frequency_days,
                     demand_std_daily, service_level=0.95):
    """
    Calculate min and max levels

    Min = reorder point
    Max = enough to cover lead time + review period + safety stock
    """

    # Min level (reorder point)
    expected_demand_lt = demand_avg_daily * lead_time_days
    z = stats.norm.ppf(service_level)
    std_during_lt = demand_std_daily * np.sqrt(lead_time_days)
    safety_stock = z * std_during_lt
    min_level = expected_demand_lt + safety_stock

    # Max level (order-up-to level)
    coverage_period = lead_time_days + review_frequency_days
    expected_demand_coverage = demand_avg_daily * coverage_period
    std_during_coverage = demand_std_daily * np.sqrt(coverage_period)
    safety_stock_max = z * std_during_coverage
    max_level = expected_demand_coverage + safety_stock_max

    return {
        'min_level': round(min_level, 0),
        'max_level': round(max_level, 0),
        'order_quantity_typical': round(max_level - min_level, 0),
        'safety_stock': round(safety_stock, 0)
    }

# Example
min_max = calculate_min_max(
    demand_avg_daily=100,
    lead_time_days=14,
    review_frequency_days=7,
    demand_std_daily=20,
    service_level=0.95
)

print(f"Min Level: {min_max['min_level']} units")
print(f"Max Level: {min_max['max_level']} units")
print(f"Typical order quantity: {min_max['order_quantity_typical']} units")

流通所要量計画(DRP)

DRPの概念:

  • 多段階ネットワークを対象とした期間別計画
  • 中央計画部門からのプッシュ方式
  • 将来の在庫状況を予測します
  • 補充注文を事前に計画します
DRPの計算
def drp_calculation(forecast, on_hand, scheduled_receipts, lead_time,
                   safety_stock, order_quantity):
    """
    Distribution Requirements Planning calculation

    Parameters:
    - forecast: Array of forecasted demand by period
    - on_hand: Starting on-hand inventory
    - scheduled_receipts: Dict {period: quantity} of scheduled orders
    - lead_time: Lead time in periods
    - safety_stock: Safety stock target
    - order_quantity: Standard order quantity

    Returns:
    - DRP table
    """

    periods = len(forecast)
    drp_table = []

    current_on_hand = on_hand

    for period in range(periods):
        # Scheduled receipt this period
        receipt = scheduled_receipts.get(period, 0)

        # Projected available before order
        proj_available_before = current_on_hand + receipt - forecast[period]

        # Check if order needed
        if proj_available_before < safety_stock:
            planned_order = order_quantity
            proj_available_after = proj_available_before + planned_order
        else:
            planned_order = 0
            proj_available_after = proj_available_before

        drp_table.append({
            'period': period + 1,
            'forecast': forecast[period],
            'scheduled_receipt': receipt,
            'proj_available_before': round(proj_available_before, 0),
            'planned_order': planned_order,
            'proj_available_after': round(proj_available_after, 0)
        })

        current_on_hand = proj_available_after

        # Schedule planned order to arrive after lead time
        if planned_order > 0 and period + lead_time < periods:
            if period + lead_time not in scheduled_receipts:
                scheduled_receipts[period + lead_time] = 0
            scheduled_receipts[period + lead_time] += planned_order

    return pd.DataFrame(drp_table)

# Example
forecast = [100, 110, 105, 120, 115, 125, 130, 135, 140, 120, 115, 110]
scheduled_receipts = {0: 500}  # Order arriving in period 0

drp_table = drp_calculation(
    forecast=forecast,
    on_hand=200,
    scheduled_receipts=scheduled_receipts,
    lead_time=2,
    safety_stock=150,
    order_quantity=500
)

print(drp_table)

需要主導型補充

プル型補充

特徴:

  • 実際の消費量/販売量をきっかけに補充します
  • 需要シグナルに柔軟に対応します
  • ブルウィップ効果を最小限に抑えます
  • リードタイムが短い場合に最も効果を発揮します
def demand_driven_replenishment(sales_data, replenishment_frequency='daily',
                               coverage_days=14, safety_stock_days=7):
    """
    Calculate demand-driven replenishment quantities

    Parameters:
    - sales_data: DataFrame with daily sales history
    - replenishment_frequency: 'daily', 'weekly', 'monthly'
    - coverage_days: Days of supply to maintain
    - safety_stock_days: Additional safety stock days

    Returns:
    - Replenishment recommendations
    """

    # Calculate average daily rate (ADR)
    lookback_days = 30
    recent_sales = sales_data[-lookback_days:]
    adr = recent_sales['sales'].mean()

    # Calculate target inventory
    target_inventory = adr * (coverage_days + safety_stock_days)

    # Current inventory position
    current_inventory = sales_data.iloc[-1]['inventory']

    # Replenishment quantity
    replen_qty = max(0, target_inventory - current_inventory)

    # Adjust for replenishment frequency
    if replenishment_frequency == 'weekly':
        replen_qty = max(0, target_inventory - current_inventory)
    elif replenishment_frequency == 'monthly':
        # Ensure full month coverage
        target_inventory = adr * 30
        replen_qty = max(0, target_inventory - current_inventory)

    return {
        'average_daily_rate': round(adr, 1),
        'target_inventory': round(target_inventory, 0),
        'current_inventory': current_inventory,
        'replenishment_quantity': round(replen_qty, 0),
        'days_of_supply_current': round(current_inventory / adr, 1) if adr > 0 else 0
    }

# Example
sales_history = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=60),
    'sales': np.random.poisson(100, 60),
    'inventory': 1500 - np.random.poisson(100, 60).cumsum()
})

replen = demand_driven_replenishment(
    sales_data=sales_history,
    replenishment_frequency='weekly',
    coverage_days=14,
    safety_stock_days=7
)

print(f"Average daily rate: {replen['average_daily_rate']}")
print(f"Current days of supply: {replen['days_of_supply_current']}")
print(f"Replenishment needed: {replen['replenishment_quantity']} units")

多段階補充

2段階モデル(DC → 店舗)

検討事項:

  • サプライヤーからDCへの補充
  • DCから店舗への補充
  • 階層間の在庫投資のバランス
  • 各階層のサービスレベル目標
def two_echelon_replenishment(dc_params, store_params, num_stores):
    """
    Calculate replenishment parameters for two-echelon system

    Parameters:
    - dc_params: Dict with DC demand and lead time from supplier
    - store_params: Dict with store demand and lead time from DC
    - num_stores: Number of stores supplied by DC

    Returns:
    - Replenishment parameters for DC and stores
    """

    # DC level
    # Aggregate demand from all stores
    dc_demand_avg = store_params['demand_avg'] * num_stores
    dc_demand_std = store_params['demand_std'] * np.sqrt(num_stores)  # Square root law

    # DC reorder point (from supplier)
    z_dc = stats.norm.ppf(dc_params['service_level'])
    dc_lt = dc_params['lead_time_from_supplier']
    dc_rop = (dc_demand_avg * dc_lt +
             z_dc * dc_demand_std * np.sqrt(dc_lt))

    # DC order quantity (EOQ or other method)
    dc_order_qty = dc_params.get('order_quantity', dc_demand_avg * 7)  # Default: 1 week

    # Store level
    # Each store maintains its own inventory
    z_store = stats.norm.ppf(store_params['service_level'])
    store_lt = store_params['lead_time_from_dc']
    store_rop = (store_params['demand_avg'] * store_lt +
                z_store * store_params['demand_std'] * np.sqrt(store_lt))

    store_order_qty = store_params.get('order_quantity',
                                       store_params['demand_avg'] * 3)  # Default: 3 days

    return {
        'dc': {
            'demand_avg': round(dc_demand_avg, 0),
            'reorder_point': round(dc_rop, 0),
            'order_quantity': round(dc_order_qty, 0),
            'safety_stock': round(z_dc * dc_demand_std * np.sqrt(dc_lt), 0)
        },
        'store': {
            'demand_avg': store_params['demand_avg'],
            'reorder_point': round(store_rop, 0),
            'order_quantity': round(store_order_qty, 0),
            'safety_stock': round(z_store * store_params['demand_std'] * np.sqrt(store_lt), 0)
        },
        'total_safety_stock': round(
            z_dc * dc_demand_std * np.sqrt(dc_lt) +
            num_stores * z_store * store_params['demand_std'] * np.sqrt(store_lt), 0
        )
    }

# Example
dc_params = {
    'lead_time_from_supplier': 14,
    'service_level': 0.98
}

store_params = {
    'demand_avg': 10,
    'demand_std': 3,
    'lead_time_from_dc': 2,
    'service_level': 0.95
}

replen_params = two_echelon_replenishment(dc_params, store_params, num_stores=100)

print("DC Replenishment:")
print(f"  Demand (from all stores): {replen_params['dc']['demand_avg']}")
print(f"  Reorder point: {replen_params['dc']['reorder_point']}")
print(f"  Order quantity: {replen_params['dc']['order_quantity']}")

print("\nStore Replenishment (each):")
print(f"  Reorder point: {replen_params['store']['reorder_point']}")
print(f"  Order quantity: {replen_params['store']['order_quantity']}")

print(f"\nTotal network safety stock: {replen_params['total_safety_stock']}")

前方ピッキングエリアへの補充

倉庫の構成:

  • 予備保管エリア(パレット単位の保管)
  • 前方ピッキングエリア(ケース/バラ単位のピッキング)
  • 予備保管エリアから前方ピッキングエリアへ補充
前方ピッキングエリアの規模設定と補充
def forward_pick_replenishment(sku_velocity_daily, pick_location_capacity,
                              replenishment_frequency='nightly',
                              target_days_supply=2):
    """
    Determine forward pick location size and replenishment

    Parameters:
    - sku_velocity_daily: Daily pick volume (eaches)
    - pick_location_capacity: Max capacity of forward location
    - replenishment_frequency: 'nightly', 'shift', 'continuous'
    - target_days_supply: Days of supply to maintain in forward location

    Returns:
    - Forward pick configuration
    """

    # Minimum forward pick size (cover until next replenishment)
    if replenishment_frequency == 'nightly':
        min_capacity_needed = sku_velocity_daily * 1.2  # 20% buffer
    elif replenishment_frequency == 'shift':
        min_capacity_needed = sku_velocity_daily * 0.5 * 1.2  # Half day
    else:  # continuous
        min_capacity_needed = sku_velocity_daily * 0.25  # 6 hours

    # Target forward pick quantity
    target_qty = min(sku_velocity_daily * target_days_supply, pick_location_capacity)

    # Replenishment trigger point
    replen_trigger = sku_velocity_daily * 0.5  # When half-day supply remains

    # Replenishment quantity
    replen_qty = target_qty - replen_trigger

    return {
        'velocity_daily': sku_velocity_daily,
        'min_capacity_needed': round(min_capacity_needed, 0),
        'recommended_capacity': round(target_qty, 0),
        'replenishment_trigger': round(replen_trigger, 0),
        'replenishment_quantity': round(replen_qty, 0),
        'replenishments_per_day': round(sku_velocity_daily / replen_qty, 1)
    }

# Example
forward_pick = forward_pick_replenishment(
    sku_velocity_daily=200,
    pick_location_capacity=500,
    replenishment_frequency='nightly',
    target_days_supply=2
)

print(f"Forward pick capacity needed: {forward_pick['recommended_capacity']}")
print(f"Replenishment trigger: {forward_pick['replenishment_trigger']}")
print(f"Replenishment quantity: {forward_pick['replenishment_quantity']}")
print(f"Replenishments per day: {forward_pick['replenishments_per_day']}")

ベンダー管理在庫(VMI)

VMIモデル:

  • サプライヤーが顧客の在庫を確認できます
  • サプライヤーが補充の判断を担当します
  • 顧客がPOS/消費データを共有します
  • 委託在庫とすることもよくあります(消費時に支払い)
VMI のメリットと導入

メリット:

  • 欠品の削減(サプライヤーが事前に管理)
  • 在庫の削減(サプライヤーが顧客全体を対象に最適化)
  • 管理業務の負担軽減(発注書の作成が不要)
  • サプライヤーの計画精度向上

導入要件:

def vmi_data_sharing_requirements():
    """
    Define data sharing requirements for VMI
    """

    requirements = {
        'customer_provides': [
            'Current inventory levels (on-hand)',
            'Point-of-sale (POS) data or consumption',
            'Scheduled receipts (orders in transit)',
            'Promotional calendar',
            'Min-max levels or service level targets',
            'Storage capacity constraints'
        ],
        'supplier_provides': [
            'Replenishment recommendations',
            'Inventory visibility dashboard',
            'Shipment notifications',
            'Performance reports (fill rate, inventory turns)'
        ],
        'technology_needs': [
            'EDI or API integration',
            'Vendor portal for data access',
            'Real-time or daily data feeds',
            'Exception alerts (stockout risk, overstock)'
        ]
    }

    return requirements

# Example VMI replenishment calculation (supplier-side)
def vmi_replenishment_calculation(current_inventory, daily_consumption,
                                 lead_time_days, target_service_level=0.95,
                                 min_order_qty=100):
    """
    Supplier calculates replenishment for VMI customer
    """

    # Historical consumption (use rolling average)
    avg_consumption = daily_consumption

    # Calculate target inventory (order-up-to level)
    coverage_days = lead_time_days + 7  # Lead time + 1 week buffer
    target_inventory = avg_consumption * coverage_days

    # Replenishment quantity
    replen_qty = max(0, target_inventory - current_inventory)

    # Apply min order quantity
    if replen_qty > 0 and replen_qty < min_order_qty:
        replen_qty = min_order_qty

    days_of_supply = current_inventory / avg_consumption if avg_consumption > 0 else 999

    return {
        'current_inventory': current_inventory,
        'days_of_supply': round(days_of_supply, 1),
        'target_inventory': round(target_inventory, 0),
        'replenishment_quantity': round(replen_qty, 0),
        'action': 'Ship order' if replen_qty >= min_order_qty else 'No action needed'
    }

# Example
vmi_replen = vmi_replenishment_calculation(
    current_inventory=450,
    daily_consumption=50,
    lead_time_days=7,
    target_service_level=0.95,
    min_order_qty=100
)

print(f"Current inventory: {vmi_replen['current_inventory']}")
print(f"Days of supply: {vmi_replen['days_of_supply']}")
print(f"Replenishment needed: {vmi_replen['replenishment_quantity']}")
print(f"Action: {vmi_replen['action']}")

補充の最適化

コストベースの最適化

トレードオフ:

  • 発注コスト(準備、輸送)
  • 保管コスト(在庫保有)
  • 欠品コスト(販売機会の損失、緊急手配)
from scipy.optimize import minimize_scalar

def optimize_replenishment_quantity(demand_annual, order_cost, holding_cost_rate,
                                   unit_cost, stockout_cost_per_unit):
    """
    Optimize order quantity considering ordering, holding, and stockout costs

    Based on EOQ with stockout costs
    """

    def total_cost(order_qty):
        """Calculate total annual cost for given order quantity"""

        # Ordering cost
        orders_per_year = demand_annual / order_qty
        annual_ordering_cost = orders_per_year * order_cost

        # Holding cost
        avg_inventory = order_qty / 2
        annual_holding_cost = avg_inventory * unit_cost * holding_cost_rate

        # Simplified stockout cost (inversely related to order qty)
        # More frequent orders = less stockout risk
        annual_stockout_cost = stockout_cost_per_unit * (1000 / order_qty)

        return annual_ordering_cost + annual_holding_cost + annual_stockout_cost

    # Optimize
    result = minimize_scalar(total_cost, bounds=(50, 5000), method='bounded')

    optimal_qty = result.x
    optimal_cost = result.fun

    # Calculate components at optimal
    orders_per_year = demand_annual / optimal_qty
    ordering_cost = orders_per_year * order_cost
    holding_cost = (optimal_qty / 2) * unit_cost * holding_cost_rate
    stockout_cost = stockout_cost_per_unit * (1000 / optimal_qty)

    return {
        'optimal_order_quantity': round(optimal_qty, 0),
        'orders_per_year': round(orders_per_year, 1),
        'total_annual_cost': round(optimal_cost, 2),
        'ordering_cost': round(ordering_cost, 2),
        'holding_cost': round(holding_cost, 2),
        'stockout_cost': round(stockout_cost, 2)
    }

# Example
optimal = optimize_replenishment_quantity(
    demand_annual=10000,
    order_cost=100,
    holding_cost_rate=0.25,
    unit_cost=50,
    stockout_cost_per_unit=500
)

print(f"Optimal order quantity: {optimal['optimal_order_quantity']}")
print(f"Orders per year: {optimal['orders_per_year']}")
print(f"Total annual cost: ${optimal['total_annual_cost']:,.2f}")

ツールとライブラリ

ソフトウェアソリューション

サプライチェーン計画:

  • SAP IBP:DRP を含む統合ビジネス計画
  • Blue Yonder (JDA):補充計画
  • Oracle ASCP:高度なサプライチェーン計画
  • Kinaxis RapidResponse:需要主導型計画
  • o9 Solutions:AI を活用した計画

補充機能を備えた WMS:

  • Manhattan Associates:高度な補充ロジック
  • Blue Yonder WMS:統合補充
  • SAP EWM:拡張倉庫管理

小売業向け:

  • Oracle Retail:店舗補充の最適化
  • Blue Yonder Luminate:小売計画
  • Relex Solutions:自動補充
Python ライブラリ
# Commonly used libraries
import numpy as np  # Numerical computations
import pandas as pd  # Data manipulation
from scipy import stats  # Statistical distributions
from scipy.optimize import minimize  # Optimization
import matplotlib.pyplot as plt  # Visualization

よくある課題と解決策

課題:ブルウィップ効果

問題:

  • 需要の変動がサプライチェーンを上流に進むほど増幅する
  • 小売でのわずかな変化が、サプライヤー側で大きな変動を引き起こす
  • 過剰在庫と欠品

解決策:

  • POS データ/消費データを共有する(注文データではなく)
  • 需要主導型の補充を導入する
  • リードタイムを短縮する
  • 発注パターンを安定させる(まとめての発注を避ける)
  • VMI または協調計画を導入する
課題:リードタイムの長期化

問題:

  • 多くの安全在庫が必要になる
  • 需要の変化への対応力が低い
  • 在庫投資が増加する

解決策:

  • サプライヤーと協力してリードタイムを短縮する
  • 重要品目には航空貨物を利用する
  • SKU ごとに安全在庫を最適化する
  • 地元のサプライヤーを利用するか、ニアショアリングを行う
  • 先送り戦略を採用する
課題:不規則な需要

問題:

  • 断続的または散発的な需要
  • 従来の計算式がうまく機能しない
  • 欠品リスクが高い、または過剰在庫が発生する

解決策:

  • 断続的需要には Croston 法などを使用する
  • 在庫を持たないことを検討する(受注時に発注)
  • 需要を集約する(製品ファミリー単位)
  • 安全在庫係数を高く設定する
  • 頻繁に見直し、調整する
課題:複数階層の複雑さ

問題:

  • 複数の階層に在庫がある(DC、店舗など)
  • 全体最適化が難しい
  • 各階層で部分最適化が起こる

解決策:

  • 複数階層の最適化モデルを使用する
  • 集中型の計画を行う(ローカルな最適化だけにしない)
  • 階層間で可視性を共有する
  • DRP または需要主導型のアプローチを採用する
  • 在庫のプーリング/集約を検討する
課題:輸送コストのバランス調整

問題:

  • 少量を頻繁に発注すると輸送コストが高くなる
  • 大量を不定期に発注すると保管コストが高くなる
  • 在庫コストと輸送コストの間にトレードオフがある

解決策:

  • 経済的発注量では両方のコストを考慮する
  • トラック積載量を最適化する(複数の SKU をまとめて発注する)
  • 複数の配送をまとめるミルクランを利用する
  • クロスドッキングで貨物を小口化する
  • 輸送を共同利用するために 3PL を活用する
課題:需要予測の不正確さ

問題:

  • 需要予測に基づいて補充する
  • 予測誤差が欠品や過剰在庫につながる
  • 計画が難しい

解決策:

  • 需要主導型に切り替える(予測ではなく実績を使用する)
  • 予測精度を向上させる(demand-forecasting を参照)
  • 予測誤差への備えとして安全在庫を増やす
  • 計画期間を短縮する
  • アジャイルなサプライチェーンを構築する(迅速な対応)

出力形式

補充戦略ドキュメント

エグゼクティブサマリー:

  • 推奨する補充方法
  • 予想される在庫削減
  • サービスレベルの改善
  • 導入要件

ティア別補充ポリシー:

サプライヤー → DC:

  • ポリシー:連続レビュー(s, Q)
  • 発注点:12,000 個
  • 発注量:10,000 個(トラック1台分)
  • リードタイム:14 日
  • 目標サービスレベル:98%
  • 月間発注回数:約9回

DC → 店舗:

  • ポリシー:定期レビュー(週次)
  • 補充目標レベル:500 個(店舗あたりの平均)
  • レビュー頻度:週次
  • リードタイム:2 日
  • 目標サービスレベル:95%

SKU レベルのパラメーター:

SKUティアポリシー発注点 / 最小値発注量 / 最大値安全在庫サービスレベル
A123DC(s,Q)8,0005,0002,50098%
A123店舗Min-Max1002004095%
B456DC(R,S)-最大 15,0003,00098%
B456店舗週次-最大 1503095%

予想されるパフォーマンス:

指標現在最適化後改善
平均在庫投資額$8M$6.5M-18.8%
在庫回転率5.26.8+1.6
充足率93%97%+4 ポイント
欠品発生頻度日数の 8%日数の 3%-5 ポイント
週あたりの発注回数4538-15.6%

導入スケジュール:

  • 1~2週目:すべての SKU の補充パラメーターを算出
  • 3~4週目:WMS/ERP システムを設定
  • 5~6週目:トレーニングと並行テスト
  • 7~8週目:本番稼働とモニタリング
  • 継続:パフォーマンスのモニタリングと調整

質問事項

詳細な情報が必要な場合:

  1. ネットワークの構造はどのようになっていますか?(ティア、拠点、物流の流れ)
  2. 現在の補充方法と課題は何ですか?
  3. 各ティアのリードタイムはどのくらいですか?
  4. 需要の特性はどのようなものですか?(安定、変動、季節性、断続的)
  5. 目標とするサービスレベルはどのくらいですか?
  6. 最低発注数量や輸送上の制約はありますか?
  7. どのシステムを導入していますか?(WMS、ERP、計画ツール)
  8. VMI やサプライヤーとの連携に関心はありますか?

関連スキル

  • inventory-optimization:安全在庫と発注点の計算
  • demand-forecasting:補充計画のための需要予測
  • multi-echelon-inventory:ネットワーク全体の在庫最適化
  • distribution-center-network:ネットワーク設計と物流の流れ
  • economic-order-quantity:発注量の最適化
  • retail-replenishment:小売業向けの補充戦略
  • supply-chain-analytics:KPI とパフォーマンスの追跡

コメント