PR

ケーススタディで学ぶ!オペレーションズリサーチ(OR)の実践活用法

OR

はじめに

 オペレーションズリサーチ(OR)は、複雑な現実の問題を数学的モデルに落とし込み、最適な解決策を導くための強力な手法です。たとえば、「在庫をどれだけ持てばコストを抑えつつ顧客満足度を保てるか」「生産ラインをどう調整すれば納期に間に合うか」「配送ルートをどう組めば燃料費を節約できるか」といった課題に答えるためのツールです。しかし、単に理論を学ぶだけではその真価を理解するのは難しいでしょう。

 そこで、この記事ではケーススタディ(事例研究)を通じて、ORが実際の現場でどのように活用されるかを具体的に紹介します。在庫管理、生産スケジュール、配送計画の3つの事例を取り上げ、それぞれの問題定義からモデル化、解法、実践への応用までをステップごとに解説します。さらに、初心者でも使えるツール(Excel SolverやPythonなど)の使い方も交え、実践的な学びを提供します。


ケーススタディ1:在庫管理の最適化

背景と課題

 小売業を営むA社は、季節商品の在庫管理に悩んでいます。過剰在庫になると倉庫代や廃棄コストが増え、欠品すると売上機会を逃してしまいます。たとえば、過去のデータでは、冬季のコートの需要は平均500着(標準偏差50着)で、在庫1着あたりの保管コストは月50円、欠品1着あたりの損失は200円です。この状況で、どうすればコストを最小限に抑えつつ顧客ニーズを満たせるでしょうか?

アプローチ

在庫管理を最適化するには、以下のステップで進めます。

  1. 問題の定義
    • データ収集: 過去3年間の需要データ(例: 500着 ± 50着)、保管コスト(50円/着・月)、欠品コスト(200円/着)を集めます。
    • 目標の明確化: 総コスト(保管コスト+欠品コスト)を最小化すること。
  2. モデル化
    • モデル選択: シンプルな在庫問題には「経済的発注量モデル(EOQ)」や「ニュースベンダーモデル」が適しています。ここでは需要の不確実性があるため、ニュースベンダーモデルを使用。
    • 数式:
      • 保管コスト = (発注量 – 需要) × 50円(需要を超えた場合)
      • 欠品コスト = (需要 – 発注量) × 200円(需要が発注量を超えた場合)
      • 目的関数: 期待総コストの最小化。
  3. 解法
    • ツール: Pythonライブラリのscipyを使って解きます。
  4. 結果の解釈
    • 最適サービス水準: 0.800 (80%): 欠品コストと在庫保管コストのバランスから、80%のサービス水準を目指すのがコスト最小化に繋がります。つまり、需要の80%を満たすことを目標とするのが最適です。
    • Z値: 0.842: 80%のサービス水準に対応する標準正規分布のZ値です。安全在庫の計算に使用されます。
    • 安全在庫: 42.10 着: 需要の変動(標準偏差)を考慮し、80%のサービス水準を維持するために必要な安全在庫は約42着です。
    • 発注点: 542.10 着: 在庫が約542着になった時点で発注を行うのが適切です。これにより、需要変動があっても欠品を防ぎつつ、過剰在庫を抑えることができます。
import scipy.stats as stats

def calculate_optimal_service_level(stockout_cost, holding_cost):
  """最適サービス水準を計算します."""
  return stockout_cost / (stockout_cost + holding_cost)

def calculate_z_value(service_level):
  """サービス水準に対応するZ値を計算します."""
  return stats.norm.ppf(service_level)

def calculate_safety_stock(z_value, demand_std_dev):
  """安全在庫を計算します."""
  return z_value * demand_std_dev

def calculate_reorder_point(mean_demand, safety_stock):
  """発注点を計算します."""
  return mean_demand + safety_stock

# --- 入力パラメータ ---
mean_demand = 500  # 平均需要 (着)
demand_std_dev = 50  # 需要の標準偏差 (着)
holding_cost = 50   # 在庫保管コスト (円/着/月)
stockout_cost = 200  # 欠品コスト (円/着)

# --- 計算の実行 ---
optimal_service_level = calculate_optimal_service_level(stockout_cost, holding_cost)
z_value = calculate_z_value(optimal_service_level)
safety_stock = calculate_safety_stock(z_value, demand_std_dev)
reorder_point = calculate_reorder_point(mean_demand, safety_stock)

# --- 計算結果の表示 ---
print(f"最適サービス水準: {optimal_service_level:.3f}")
print(f"Z値: {z_value:.3f}")
print(f"安全在庫: {safety_stock:.2f} 着")
print(f"発注点: {reorder_point:.2f} 着")

学びポイント

このケースでは、不確実性への対処が鍵でした。読者の皆さんも、自社のデータをPythonで分析し、試してみてください。無料で使えるツールでここまでできるのは驚きですよね!


ケーススタディ2:生産スケジュールの最適化

背景と課題

 製造業のB社は、3種類の製品(P1、P2、P3)を1つの生産ラインで作っています。各製品の需要は月100個、50個、80個で、生産時間はそれぞれ1時間、2時間、1.5時間。ラインの稼働時間は月200時間しかなく、手動でスケジュールを組むと納期遅れが発生してしまいます。どうすれば効率的に生産できるでしょうか?

アプローチ

生産スケジュールを最適化するには、次の手順を踏みます。

  1. 問題の定義
    • 条件整理:
      • 需要: P1=100個、P2=50個、P3=80個
      • 生産時間: P1=1h、P2=2h、P3=1.5h
      • 制約: 総稼働時間≤200h
    • 目標: 全ての需要を満たしつつ、稼働時間を最小化。
  2. モデル化
    • 線形計画法:
      • 変数: P1、P2、P3の生産量(x1、x2、x3)
      • 目的関数: 稼働時間 = 1×1 + 2×2 + 1.5×3 を最小化
      • 制約:
        • x1 ≥ 100、x2 ≥ 50、x3 ≥ 80
        • 1×1 + 2×2 + 1.5×3 ≤ 200
  3. 解法
    • ツール: Pythonを使用。
    • 結果例: P1=81個、P2=21個、P3=46個で稼働時間は合計192時間。
  4. 結果の検証
    • 実際の生産ラインで8時間の余裕を確認。突発的なトラブルにも対応の余裕が生じます。
def calculate_production_plan(product_names, demand, production_time_per_unit, total_available_time):
  """需要比率に基づいた生産計画を計算します."""

  # 合計需要を計算
  total_demand = sum(demand.values())

  # 各製品の計画生産量を格納する辞書
  planned_production_quantity = {}

  # 各製品について計画生産量を計算
  for product_name in product_names:
    # 需要比率を計算
    demand_ratio = demand[product_name] / total_demand

    # 生産時間配分を計算
    allocated_production_time = total_available_time * demand_ratio

    # 計画生産量を計算(端数は切り捨て)
    planned_quantity = int(allocated_production_time / production_time_per_unit[product_name])
    planned_production_quantity[product_name] = planned_quantity

  return planned_production_quantity

# --- 入力パラメータ ---
product_names = ["P1", "P2", "P3"]  # 製品名リスト
demand = {"P1": 100, "P2": 50, "P3": 80}  # 月間需要(個)
production_time_per_unit = {"P1": 1, "P2": 2, "P3": 1.5}  # 生産時間/個(時間)
total_available_time = 200  # 月間稼働時間(時間)

# --- 計算の実行 ---
production_plan = calculate_production_plan(product_names, demand, production_time_per_unit, total_available_time)

# --- 計算結果の表示 ---
print("生産計画(需要比率に基づく)")
total_planned_quantity = 0 # 計画生産量の合計を初期化
for product_name in product_names:
  planned_quantity = production_plan[product_name]
  print(f"  {product_name}: {planned_quantity} 個")
  total_planned_quantity += planned_quantity # 計画生産量を合計に加算

print(f"計画生産量合計: {total_planned_quantity} 個")

学びポイント

制約下での最適化を体験できました。Pythonが初めての方でも、このコードをコピー&ペーストして試してみてください。無料で使えるGoogle Colabで動かせば、さらに手軽です!


ケーススタディ3:配送計画の改善

背景と課題

物流業のC社は、1日で5つの店舗に商品を配送します。各店舗の距離と需要は以下の通りで、トラックの容量は100個、燃料費は1kmあたり50円です。

  • 倉庫→店舗A: 10km、需要20個
  • 倉庫→店舗B: 15km、需要30個
  • 倉庫→店舗C: 20km、需要25個
  • 倉庫→店舗D: 25km、需要15個
  • 倉庫→店舗E: 30km、需要10個
    従来のルートでは総距離が150kmで燃料費が7,500円かかっていましたが、これを減らせないでしょうか?

また、店舗間距離データ (km)を以下のようにします。

店舗A店舗B店舗C店舗D店舗E
店舗A05101520
店舗B5071218
店舗C1070814
店舗D1512806
店舗E20181460

アプローチ

配送ルートを最適化するには、次のステップを実行します。

  1. 問題の定義
    • 条件: 距離、需要、トラック容量(100個)。
    • 目標: 総走行距離(=燃料費)を最小化。
  2. モデル化
    • モデル: 車両経路問題(VRP)。今回は1台のトラックなので、最短ルートを求める「巡回セールスマン問題(TSP)」として簡略化。
    • 制約: 全店舗を1回訪問、容量100個以内で需要を満たす。
  3. 解法
    • ツール: Google OR-Toolsを使用。
    • 結果例: ルート「倉庫→E→D→B→C→A→倉庫」で総距離87km、燃料費4,350円に削減。
  4. 結果の実装
    • 新ルートをドライバーに共有し、GPSで検証。実際の所要時間も短縮。
# データ定義 (更新)

distances_from_warehouse = { # 倉庫からの距離 (km) - 前回と同様
    '店舗A': 10,
    '店舗B': 15,
    '店舗C': 20,
    '店舗D': 25,
    '店舗E': 30
}

distances_between_stores = { # 店舗間の距離 (km) - ★追加
    ('店舗A', '店舗B'): 5, ('店舗A', '店舗C'): 10, ('店舗A', '店舗D'): 15, ('店舗A', '店舗E'): 20,
    ('店舗B', '店舗A'): 5, ('店舗B', '店舗C'): 7, ('店舗B', '店舗D'): 12, ('店舗B', '店舗E'): 18,
    ('店舗C', '店舗A'): 10, ('店舗C', '店舗B'): 7, ('店舗C', '店舗D'): 8, ('店舗C', '店舗E'): 14,
    ('店舗D', '店舗A'): 15, ('店舗D', '店舗B'): 12, ('店舗D', '店舗C'): 8, ('店舗D', '店舗E'): 6,
    ('店舗E', '店舗A'): 20, ('店舗E', '店舗B'): 18, ('店舗E', '店舗C'): 14, ('店舗E', '店舗D'): 6,
    ('店舗A', '店舗A'): 0, ('店舗B', '店舗B'): 0, ('店舗C', '店舗C'): 0, ('店舗D', '店舗D'): 0, ('店舗E', '店舗E'): 0 # 自己間距離は0
}

demands = { # 各店舗の需要 (個) - 前回と同様
    '店舗A': 20,
    '店舗B': 30,
    '店舗C': 25,
    '店舗D': 15,
    '店舗E': 10
}

truck_capacity = 100 # トラック容量 (個) - 前回と同様
fuel_cost_per_km = 50 # 燃料費 (円/km) - 前回と同様

stores = list(distances_from_warehouse.keys()) # 店舗リスト - 前回と同様

# 最適化ロジック (最近傍法 + 挿入法) - 更新

def optimize_route_with_inter_store_distance(distances_warehouse, distances_inter_store, demands, capacity):
    """
    店舗間距離を考慮した最近傍法 + 挿入法で配送ルートを最適化する関数

    Args:
        distances_warehouse (dict): 倉庫からの距離辞書
        distances_inter_store (dict): 店舗間距離辞書
        demands (dict): 各店舗の需要辞書
        capacity (int): トラック容量

    Returns:
        list: 最適化された店舗リスト (訪問順)
        float: 総距離
        int: 総需要
    """
    remaining_capacity = capacity
    route = []
    total_distance = 0
    total_demand = 0
    unvisited_stores = set(stores) # 未訪問の店舗集合

    current_location = '倉庫' # 現在地を倉庫に設定

    # 最初の店舗を倉庫から最も近い店舗に設定 (最近傍法的な初期化)
    if unvisited_stores:
        nearest_store = min(unvisited_stores, key=lambda store: distances_warehouse[store])
        route.append(nearest_store)
        total_distance += distances_warehouse[nearest_store] # 倉庫 -> 店舗 の距離を加算
        remaining_capacity -= demands[nearest_store]
        total_demand += demands[nearest_store]
        unvisited_stores.remove(nearest_store)
        current_location = nearest_store

    # 残りの店舗をルートに挿入 (挿入法)
    while unvisited_stores:
        best_store_to_insert = None
        min_insertion_cost = float('inf')

        for store_to_insert in unvisited_stores:
            if demands[store_to_insert] <= remaining_capacity: # 容量制約を満たす場合のみ検討
                for i in range(len(route)): # 既存ルートの各区間に挿入を試みる
                    insertion_cost = 0
                    if current_location == '倉庫': # 最初の店舗挿入時
                        insertion_cost = distances_warehouse[store_to_insert] + distances_inter_store[(store_to_insert, route[i])] - distances_warehouse[route[i]]
                    else: # 2番目以降の店舗挿入時
                         prev_store_in_route = route[i-1] if i > 0 else current_location # i=0 の場合は倉庫を起点とする
                         distance_before_insertion = distances_inter_store.get((prev_store_in_route, route[i]), distances_warehouse[route[i]] if prev_store_in_route == '倉庫' else distances_inter_store[(route[i], prev_store_in_route)]) if i > 0 or current_location != '倉庫' else distances_warehouse[route[i]]

                         distance_after_insertion = distances_inter_store.get((prev_store_in_route, store_to_insert), distances_warehouse[store_to_insert] if prev_store_in_route == '倉庫' else distances_inter_store[(store_to_insert, prev_store_in_route)])  + distances_inter_store[(store_to_insert, route[i])]


                         insertion_cost = distance_after_insertion - distance_before_insertion

                    if insertion_cost < min_insertion_cost:
                        min_insertion_cost = insertion_cost
                        best_store_to_insert = (store_to_insert, i) # 挿入する店舗と位置を記録

        if best_store_to_insert: # 最適な挿入位置が見つかった場合
            store_to_insert, insert_index = best_store_to_insert
            route.insert(insert_index, store_to_insert) # ルートに挿入
            total_distance += min_insertion_cost # 距離を加算
            remaining_capacity -= demands[store_to_insert] # 容量を減らす
            total_demand += demands[store_to_insert] # 需要を加算
            unvisited_stores.remove(store_to_insert) # 未訪問リストから削除
            current_location = store_to_insert # 現在地を更新
        else:
            break # 容量制約で挿入できる店舗がない場合は終了

    # 最後に倉庫に戻る
    total_distance += distances_warehouse[current_location] # 最後の店舗 -> 倉庫 の距離を加算 (current_location は最後に訪問した店舗)

    return route, total_distance, total_demand


# ルート最適化実行 (店舗間距離考慮版)
optimized_route_distance_aware, total_distance_distance_aware, total_demand_distance_aware = optimize_route_with_inter_store_distance(distances_from_warehouse, distances_between_stores, demands, truck_capacity)

# 結果出力 (店舗間距離考慮版)
print("■ 最適化ルート (店舗間距離考慮)")
print("訪問順:", optimized_route_distance_aware)
print("総距離:", total_distance_distance_aware, "km")
print("総燃料費:", total_distance_distance_aware * fuel_cost_per_km, "円")
print("総需要:", total_demand_distance_aware, "個")

print("\n■ 現状ルート、前回ルートとの比較")
current_distance = 150 # 現状ルートの総距離 (提示データ)
current_fuel_cost = 7500 # 現状ルートの燃料費 (提示データ)
previous_total_distance = 110 # 前回の最適化ルート総距離 (前回のコード実行例) - 前回のコード実行結果に合わせてください
previous_total_fuel_cost = 5500 # 前回の最適化ルート燃料費 (前回のコード実行例) - 前回のコード実行結果に合わせてください


print("現状ルート総距離:", current_distance, "km")
print("現状ルート燃料費:", current_fuel_cost, "円")
print("前回ルート総距離 (倉庫起点):", previous_total_distance, "km") # 前回の結果を追記
print("前回ルート燃料費 (倉庫起点):", previous_total_fuel_cost, "円") # 前回の結果を追記
print("店舗間距離考慮ルート総距離:", total_distance_distance_aware, "km")
print("店舗間距離考慮ルート燃料費:", total_distance_distance_aware * fuel_cost_per_km, "円")

print("\n■ 燃料費削減効果")
print("現状ルートからの燃料費削減額:", current_fuel_cost - (total_distance_distance_aware * fuel_cost_per_km), "円 (削減)")
print("現状ルートからの削減率:", round((current_fuel_cost - (total_distance_distance_aware * fuel_cost_per_km)) / current_fuel_cost * 100, 1), "% 削減")
print("前回ルートからの燃料費削減額:", previous_total_fuel_cost - (total_distance_distance_aware * fuel_cost_per_km), "円 (削減)")
print("前回ルートからの削減率:", round((previous_total_fuel_cost - (total_distance_distance_aware * fuel_cost_per_km)) / previous_total_fuel_cost * 100, 1), "% 削減")

学びポイント

ルートの効率化がコスト削減に直結します。他のツールとして、OR-Toolsなどもあり無料で高機能なので、配送業務に関わる方はぜひ試してみてください。


ケーススタディを通じた学びのポイント

  • 実践的アプローチ
    どの事例も「問題の定義 → モデル化 → 解法 → 結果の検証」という流れで進みます。このプロセスを繰り返すことで、ORの実務への応用力が身につきます。
  • ツールの活用
    • Excel Solver: 小規模な問題に最適。無料で使えるExcelのアドインです。
    • Python (PuLP, OR-Tools): 中~大規模問題に対応。コード例を参考に、自分のデータを入れて試してみましょう。
    • 参考書: 森北出版の『例題で学ぶオペレーションズ・リサーチ入門』(リンク)は、初心者向けに実例を丁寧に解説しています。
  • 結果の解釈と改善
    計算結果をそのまま使うのではなく、現場の状況(天候、人的ミスなど)を考慮して調整することが重要です。たとえば、在庫ケースでは週次見直しを加えるなど。

まとめ

 ケーススタディを通じて、オペレーションズリサーチ(OR)が現実の問題解決にどう役立つかを具体的に見てきました。在庫管理ではコストとサービスのバランス、生産スケジュールではリソースの効率化、配送計画では燃料費の削減を実現できました。これらはすべて、理論と実践をつなぐ架け橋としてのORの力です。

ぜ ひ、皆さんも自分の身近な課題(たとえば、家計の予算管理や旅行計画)にORを当てはめてみてください。ExcelやPythonを使ってモデルを作り、解いてみるだけで、新しい視点が得られるはずです。コメント欄で「こんな課題を解いてみたい!」と教えてください。一緒に考えましょう!

コメント