はじめに
オペレーションズリサーチ(OR)は、複雑な現実の問題を数学的モデルに落とし込み、最適な解決策を導くための強力な手法です。たとえば、「在庫をどれだけ持てばコストを抑えつつ顧客満足度を保てるか」「生産ラインをどう調整すれば納期に間に合うか」「配送ルートをどう組めば燃料費を節約できるか」といった課題に答えるためのツールです。しかし、単に理論を学ぶだけではその真価を理解するのは難しいでしょう。
そこで、この記事ではケーススタディ(事例研究)を通じて、ORが実際の現場でどのように活用されるかを具体的に紹介します。在庫管理、生産スケジュール、配送計画の3つの事例を取り上げ、それぞれの問題定義からモデル化、解法、実践への応用までをステップごとに解説します。さらに、初心者でも使えるツール(Excel SolverやPythonなど)の使い方も交え、実践的な学びを提供します。
ケーススタディ1:在庫管理の最適化
背景と課題
小売業を営むA社は、季節商品の在庫管理に悩んでいます。過剰在庫になると倉庫代や廃棄コストが増え、欠品すると売上機会を逃してしまいます。たとえば、過去のデータでは、冬季のコートの需要は平均500着(標準偏差50着)で、在庫1着あたりの保管コストは月50円、欠品1着あたりの損失は200円です。この状況で、どうすればコストを最小限に抑えつつ顧客ニーズを満たせるでしょうか?

アプローチ
在庫管理を最適化するには、以下のステップで進めます。
- 問題の定義
- データ収集: 過去3年間の需要データ(例: 500着 ± 50着)、保管コスト(50円/着・月)、欠品コスト(200円/着)を集めます。
- 目標の明確化: 総コスト(保管コスト+欠品コスト)を最小化すること。
- モデル化
- モデル選択: シンプルな在庫問題には「経済的発注量モデル(EOQ)」や「ニュースベンダーモデル」が適しています。ここでは需要の不確実性があるため、ニュースベンダーモデルを使用。
- 数式:
- 保管コスト = (発注量 – 需要) × 50円(需要を超えた場合)
- 欠品コスト = (需要 – 発注量) × 200円(需要が発注量を超えた場合)
- 目的関数: 期待総コストの最小化。
- 解法
- ツール: Pythonライブラリのscipyを使って解きます。
- 結果の解釈
- 最適サービス水準: 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時間しかなく、手動でスケジュールを組むと納期遅れが発生してしまいます。どうすれば効率的に生産できるでしょうか?

アプローチ
生産スケジュールを最適化するには、次の手順を踏みます。
- 問題の定義
- 条件整理:
- 需要: P1=100個、P2=50個、P3=80個
- 生産時間: P1=1h、P2=2h、P3=1.5h
- 制約: 総稼働時間≤200h
- 目標: 全ての需要を満たしつつ、稼働時間を最小化。
- 条件整理:
- モデル化
- 線形計画法:
- 変数: 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
- 線形計画法:
- 解法
- ツール: Pythonを使用。
- 結果例: P1=81個、P2=21個、P3=46個で稼働時間は合計192時間。
- 結果の検証
- 実際の生産ラインで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 | |
---|---|---|---|---|---|
店舗A | 0 | 5 | 10 | 15 | 20 |
店舗B | 5 | 0 | 7 | 12 | 18 |
店舗C | 10 | 7 | 0 | 8 | 14 |
店舗D | 15 | 12 | 8 | 0 | 6 |
店舗E | 20 | 18 | 14 | 6 | 0 |

アプローチ
配送ルートを最適化するには、次のステップを実行します。
- 問題の定義
- 条件: 距離、需要、トラック容量(100個)。
- 目標: 総走行距離(=燃料費)を最小化。
- モデル化
- モデル: 車両経路問題(VRP)。今回は1台のトラックなので、最短ルートを求める「巡回セールスマン問題(TSP)」として簡略化。
- 制約: 全店舗を1回訪問、容量100個以内で需要を満たす。
- 解法
- ツール: Google OR-Toolsを使用。
- 結果例: ルート「倉庫→E→D→B→C→A→倉庫」で総距離87km、燃料費4,350円に削減。
- 結果の実装
- 新ルートをドライバーに共有し、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を使ってモデルを作り、解いてみるだけで、新しい視点が得られるはずです。コメント欄で「こんな課題を解いてみたい!」と教えてください。一緒に考えましょう!
コメント