warehouse-location-optimization について
warehouse-location-optimizationは、物流コスト、容量、サービス要件のバランスを考える倉庫立地・配送ネットワーク設計のスキルです。混合整数計画、重心法、カバレッジモデルに加え、貪欲法や節約ベースのヒューリスティックを扱います。
主な機能
- 01複数製品の倉庫立地モデル
- 02容量・サービス制約のモデル化
- 03PuLPによる混合整数計画
- 04重心法とカバレッジ立地手法
- 05貪欲な開設・統合ヒューリスティック
活用シーン
- 01新設または既存の倉庫網の計画
- 02固定費と輸送費のトレードオフ検討
- 03納期やサービス範囲の要件への対応
- 04季節需要に応じた容量の評価
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ kishorkukreja/ awesome-supply-chain --skill warehouse-location-optimizationインストール後、Claude Code で/warehouse-location-optimizationを入力して読み込まれたか確認します。
詳細
倉庫立地の最適化
倉庫立地の最適化と配送ネットワーク設計の専門家として、サービス要件と容量制約を満たしながら、物流コストの総額を最小限に抑える最適な倉庫の立地とネットワーク構成を特定します。
初期評価
倉庫立地を最適化する前に、次の点を把握してください。
-
ネットワークの範囲
- グリーンフィールド(新規ネットワーク)ですか、それともブラウンフィールド(既存施設)ですか?
- 全国規模、地域規模、それともグローバルネットワークですか?
- 単一階層型の配送ですか、それとも多階層型の配送ですか?
- 既存倉庫と新設候補倉庫はそれぞれいくつありますか?
-
倉庫の特性
- 倉庫の種類は何ですか?(地域倉庫、地域内倉庫、フルフィルメントセンター)
- 容量制約はありますか?(保管、処理能力)
- 固定費はいくらですか?(賃貸料、建設費、設備費)
- 運営費はいくらですか?(人件費、光熱費、管理費)
- 倉庫の規模はどれくらいですか?(小規模、中規模、大規模)
-
需要の特性
- 顧客の所在地と需要量はどれくらいですか?
- 需要の変動性や季節性はありますか?
- 製品の種類と保管要件は何ですか?
- サービスレベルの要件は何ですか?(配送時間、充足率)
- 注文の特性は何ですか?(B2B、B2C、オムニチャネル)
-
供給元
- 製造工場やサプライヤーはありますか?
- 港を経由した輸出入はありますか?
- クロスドッキングの要件はありますか?
- 入荷輸送の輸送手段は何ですか?
-
コスト要素
- 倉庫の固定費はいくらですか?(賃貸料、資本費)
- 運営費はいくらですか?(人件費、光熱費、管理費)
- 入荷輸送費はいくらですか?(サプライヤー → 倉庫)
- 出荷輸送費はいくらですか?(倉庫 → 顧客)
- 在庫保有コストはいくらですか?
- サービス未達時のペナルティはいくらですか?
倉庫立地の意思決定フレームワーク
戦略的な意思決定
長期(3~10年):
- ネットワーク内の倉庫数
- 倉庫の立地(地理的な配置)
- 倉庫の規模と種類
- テクノロジーへの投資
- ネットワーク構造の設計
中期(1~3年):
- 容量の調整
- 賃借か所有かの意思決定
- 3PLとの提携
- 季節ごとの容量計画
短期(1年未満):
- 在庫の割り当て
- 注文のフルフィルメント先の割り当て
- ルート設定とスケジュール作成
主なトレードオフ
固定費と輸送費:
- 倉庫が多い → 固定費が増加
- 倉庫が多い → 顧客との距離が近くなり、出荷輸送費が低下
- 倉庫が少ない → 固定費が低下し、輸送費が増加
在庫とサービス:
- 倉庫が多い → 必要な安全在庫が増加
- 集中型 → 在庫は減少する一方、サービスが遅くなる可能性がある
- 分散型 → 在庫は増加する一方、サービスが速くなる
柔軟性と効率性:
- 小規模な倉庫が多数 → 柔軟性は高まる一方、コストが増加
- 大規模な倉庫が少数 → 規模の経済が得られる一方、柔軟性が低下
数理モデル
複数製品の倉庫立地モデル
集合:
- I:倉庫の立地候補の集合
- J:顧客の集合
- K:製品の集合
- S:サプライヤー/供給元の集合
パラメーター:
- f_i:立地 i に倉庫を開設する固定費
- Q_i:倉庫 i の容量(保管量または処理能力)
- d_{jk}:顧客 j の製品 k に対する需要量
- c_{ij}:倉庫 i から顧客 j への単位輸送費
- c_{si}:サプライヤー s から倉庫 i への単位入荷費
- h_k:製品 k の在庫保有コスト
- α:在庫係数(安全在庫係数)
意思決定変数:
- y_i ∈ {0,1}:立地 i の倉庫を開設する場合は 1
- x_{ijk} ∈ [0,1]:顧客 j の製品 k に対する需要のうち、倉庫 i が供給する割合
- z_{sik}:サプライヤー s から倉庫 i への製品 k の流量
目的関数:
Minimize:
Fixed costs:
Σ_i f_i × y_i
+ Outbound transportation:
Σ_i Σ_j Σ_k c_{ij} × d_{jk} × x_{ijk}
+ Inbound transportation:
Σ_s Σ_i Σ_k c_{si} × z_{sik}
+ Inventory holding:
α × Σ_i Σ_k h_k × (Σ_j d_{jk} × x_{ijk})
制約条件:
1. Demand satisfaction:
Σ_i x_{ijk} = 1, ∀j ∈ J, ∀k ∈ K
2. Warehouse capacity:
Σ_j Σ_k d_{jk} × x_{ijk} ≤ Q_i × y_i, ∀i ∈ I
3. Serve only from open warehouses:
x_{ijk} ≤ y_i, ∀i ∈ I, ∀j ∈ J, ∀k ∈ K
4. Inbound-outbound flow balance:
Σ_s z_{sik} = Σ_j d_{jk} × x_{ijk}, ∀i ∈ I, ∀k ∈ K
5. Binary and non-negativity:
y_i ∈ {0,1}, ∀i ∈ I
x_{ijk} ≥ 0, ∀i,j,k
z_{sik} ≥ 0, ∀s,i,k
サービス制約付き倉庫立地
追加パラメーター:
- T_j:顧客 j が許容できる最大配送時間
- t_{ij}:倉庫 i から顧客 j への配送時間
サービス制約:
Only serve customer j from warehouse i if delivery time acceptable:
x_{ijk} = 0 if t_{ij} > T_j
または制約条件として:
x_{ijk} ≤ y_i × I(t_{ij} ≤ T_j), ∀i,j,k
where I(condition) = 1 if condition true, 0 otherwise
解法
1. PuLP による MIP モデル
from pulp import *
import numpy as np
import pandas as pd
def solve_warehouse_location(warehouse_data, customer_data,
transport_costs_out, transport_costs_in=None,
supplier_locations=None, products=None):
"""
Solve multi-product warehouse location problem
Args:
warehouse_data: DataFrame with columns [warehouse_id, fixed_cost, capacity]
customer_data: DataFrame with columns [customer_id, demand, lat, lon]
transport_costs_out: cost matrix [warehouses x customers] or per-distance rate
transport_costs_in: optional inbound costs
supplier_locations: optional supplier data
products: optional product list
Returns:
optimal solution
"""
n_warehouses = len(warehouse_data)
n_customers = len(customer_data)
# Create problem
prob = LpProblem("Warehouse_Location", LpMinimize)
# Decision variables
# y[i] = 1 if warehouse i is opened
y = LpVariable.dicts("warehouse",
warehouse_data.index,
cat='Binary')
# x[i,j] = fraction of customer j served by warehouse i
x = LpVariable.dicts("service",
[(i, j) for i in warehouse_data.index
for j in customer_data.index],
lowBound=0, upBound=1, cat='Continuous')
# Objective: Minimize total cost
# Fixed costs
fixed_cost_expr = lpSum([
warehouse_data.loc[i, 'fixed_cost'] * y[i]
for i in warehouse_data.index
])
# Transportation costs
transport_cost_expr = lpSum([
transport_costs_out[i][j] * customer_data.loc[j, 'demand'] * x[i,j]
for i in warehouse_data.index
for j in customer_data.index
])
prob += fixed_cost_expr + transport_cost_expr, "Total_Cost"
# Constraints
# 1. Each customer fully served
for j in customer_data.index:
prob += (
lpSum([x[i,j] for i in warehouse_data.index]) == 1,
f"Demand_Customer_{j}"
)
# 2. Warehouse capacity constraints
for i in warehouse_data.index:
prob += (
lpSum([customer_data.loc[j, 'demand'] * x[i,j]
for j in customer_data.index]) <=
warehouse_data.loc[i, 'capacity'] * y[i],
f"Capacity_Warehouse_{i}"
)
# 3. Serve only from open warehouses
for i in warehouse_data.index:
for j in customer_data.index:
prob += (
x[i,j] <= y[i],
f"Open_{i}_{j}"
)
# Solve
import time
start_time = time.time()
prob.solve(PULP_CBC_CMD(msg=1, timeLimit=600))
solve_time = time.time() - start_time
# Extract solution
if LpStatus[prob.status] in ['Optimal', 'Feasible']:
open_warehouses = [i for i in warehouse_data.index
if y[i].varValue > 0.5]
# Customer assignments
assignments = {}
for j in customer_data.index:
assignments[j] = []
for i in warehouse_data.index:
if x[i,j].varValue > 0.01:
assignments[j].append({
'warehouse': i,
'fraction': x[i,j].varValue
})
# Calculate warehouse utilization
utilization = {}
for i in open_warehouses:
used_capacity = sum(
customer_data.loc[j, 'demand'] * x[i,j].varValue
for j in customer_data.index
)
utilization[i] = (used_capacity /
warehouse_data.loc[i, 'capacity'] * 100)
# Cost breakdown
total_fixed_cost = sum(
warehouse_data.loc[i, 'fixed_cost']
for i in open_warehouses
)
total_transport_cost = sum(
transport_costs_out[i][j] *
customer_data.loc[j, 'demand'] *
x[i,j].varValue
for i in warehouse_data.index
for j in customer_data.index
)
return {
'status': LpStatus[prob.status],
'total_cost': value(prob.objective),
'fixed_cost': total_fixed_cost,
'transport_cost': total_transport_cost,
'open_warehouses': open_warehouses,
'num_warehouses': len(open_warehouses),
'assignments': assignments,
'utilization': utilization,
'solve_time': solve_time
}
else:
return {
'status': LpStatus[prob.status],
'solve_time': solve_time
}
# Example usage
if __name__ == "__main__":
import numpy as np
import pandas as pd
np.random.seed(42)
# Warehouse data: 8 potential locations
warehouse_data = pd.DataFrame({
'warehouse_id': range(8),
'fixed_cost': [500000, 450000, 600000, 520000,
480000, 550000, 490000, 530000],
'capacity': [10000, 8000, 12000, 9000, 8500, 11000, 9500, 10500],
'lat': np.random.uniform(30, 45, 8),
'lon': np.random.uniform(-120, -70, 8)
})
warehouse_data.index = warehouse_data['warehouse_id']
# Customer data: 30 customers
customer_data = pd.DataFrame({
'customer_id': range(30),
'demand': np.random.uniform(100, 500, 30),
'lat': np.random.uniform(30, 45, 30),
'lon': np.random.uniform(-120, -70, 30)
})
customer_data.index = customer_data['customer_id']
# Calculate transportation costs (simplified: Euclidean distance × rate)
transport_rate = 0.5 # $ per unit per distance unit
transport_costs_out = np.zeros((len(warehouse_data), len(customer_data)))
for i in warehouse_data.index:
for j in customer_data.index:
distance = np.sqrt(
(warehouse_data.loc[i, 'lat'] - customer_data.loc[j, 'lat'])**2 +
(warehouse_data.loc[i, 'lon'] - customer_data.loc[j, 'lon'])**2
)
transport_costs_out[i][j] = distance * transport_rate
print("="*70)
print("WAREHOUSE LOCATION OPTIMIZATION")
print("="*70)
print(f"Potential warehouses: {len(warehouse_data)}")
print(f"Customers: {len(customer_data)}")
print(f"Total demand: {customer_data['demand'].sum():,.0f} units")
# Solve
result = solve_warehouse_location(warehouse_data, customer_data,
transport_costs_out)
print(f"\n{'='*70}")
print(f"SOLUTION")
print(f"{'='*70}")
print(f"Status: {result['status']}")
print(f"Total Cost: ${result['total_cost']:,.2f}")
print(f" Fixed Costs: ${result['fixed_cost']:,.2f} "
f"({result['fixed_cost']/result['total_cost']*100:.1f}%)")
print(f" Transport Costs: ${result['transport_cost']:,.2f} "
f"({result['transport_cost']/result['total_cost']*100:.1f}%)")
print(f"\nWarehouses Opened: {result['num_warehouses']}")
print(f"Warehouse IDs: {result['open_warehouses']}")
print(f"\nWarehouse Utilization:")
for wh_id in result['open_warehouses']:
capacity = warehouse_data.loc[wh_id, 'capacity']
util = result['utilization'][wh_id]
print(f" Warehouse {wh_id}: {util:.1f}% (capacity={capacity:,.0f})")
print(f"\nSolve Time: {result['solve_time']:.2f} seconds")
2. 重力モデルによる立地選定
def gravity_location_model(customer_locations, customer_demands,
num_warehouses=None):
"""
Gravity/Center-of-Gravity model for warehouse location
Places warehouses at demand-weighted centroids
Simple but effective heuristic for initial solutions
Args:
customer_locations: array of [lat, lon] for each customer
customer_demands: customer demand weights
num_warehouses: number of warehouses (if None, finds single location)
Returns:
optimal warehouse location(s)
"""
from sklearn.cluster import KMeans
customer_locations = np.array(customer_locations)
customer_demands = np.array(customer_demands)
if num_warehouses is None or num_warehouses == 1:
# Single warehouse: weighted centroid
total_demand = customer_demands.sum()
center_lat = (customer_locations[:, 0] * customer_demands).sum() / total_demand
center_lon = (customer_locations[:, 1] * customer_demands).sum() / total_demand
return np.array([[center_lat, center_lon]])
else:
# Multiple warehouses: use weighted K-means clustering
kmeans = KMeans(n_clusters=num_warehouses, random_state=42)
# Weight samples by demand (repeat points proportional to demand)
weights = (customer_demands / customer_demands.min()).astype(int)
weighted_locations = []
for i, loc in enumerate(customer_locations):
weighted_locations.extend([loc] * weights[i])
weighted_locations = np.array(weighted_locations)
# Fit clustering
kmeans.fit(weighted_locations)
return kmeans.cluster_centers_
# Example usage
customer_locs = np.random.rand(50, 2) * 100 # 50 customers
customer_dems = np.random.uniform(100, 1000, 50)
# Find optimal locations for 3 warehouses
warehouse_locations = gravity_location_model(customer_locs, customer_dems,
num_warehouses=3)
print("Optimal warehouse locations (gravity model):")
for i, loc in enumerate(warehouse_locations):
print(f" Warehouse {i+1}: ({loc[0]:.2f}, {loc[1]:.2f})")
3. カバー率に基づく立地選定
def coverage_based_warehouse_location(customer_locations, service_radius,
potential_warehouses=None):
"""
Warehouse location with service coverage constraints
Ensure all customers within service radius of at least one warehouse
Args:
customer_locations: customer coordinates
service_radius: maximum service distance
potential_warehouses: candidate warehouse locations
(if None, use customer locations)
Returns:
minimum set of warehouses for full coverage
"""
if potential_warehouses is None:
potential_warehouses = customer_locations
n_warehouses = len(potential_warehouses)
n_customers = len(customer_locations)
# Calculate coverage matrix
# coverage[i][j] = 1 if warehouse i can serve customer j
coverage = np.zeros((n_warehouses, n_customers))
for i in range(n_warehouses):
for j in range(n_customers):
distance = np.linalg.norm(
potential_warehouses[i] - customer_locations[j]
)
if distance <= service_radius:
coverage[i][j] = 1
# Solve set covering problem
prob = LpProblem("Coverage_Warehouse_Location", LpMinimize)
# Decision variables: y[i] = 1 if warehouse i is opened
y = LpVariable.dicts("warehouse", range(n_warehouses), cat='Binary')
# Objective: Minimize number of warehouses
prob += lpSum([y[i] for i in range(n_warehouses)]), "Num_Warehouses"
# Constraints: Each customer covered by at least one warehouse
for j in range(n_customers):
prob += (
lpSum([coverage[i][j] * y[i] for i in range(n_warehouses)]) >= 1,
f"Coverage_Customer_{j}"
)
# Solve
prob.solve(PULP_CBC_CMD(msg=0))
if LpStatus[prob.status] in ['Optimal', 'Feasible']:
open_warehouses = [i for i in range(n_warehouses)
if y[i].varValue > 0.5]
# Determine which warehouse serves each customer
assignments = {}
for j in range(n_customers):
# Assign to nearest warehouse that can serve
min_distance = float('inf')
assigned_warehouse = None
for i in open_warehouses:
if coverage[i][j] == 1:
distance = np.linalg.norm(
potential_warehouses[i] - customer_locations[j]
)
if distance < min_distance:
min_distance = distance
assigned_warehouse = i
assignments[j] = assigned_warehouse
return {
'status': LpStatus[prob.status],
'num_warehouses': len(open_warehouses),
'open_warehouses': open_warehouses,
'warehouse_locations': [potential_warehouses[i]
for i in open_warehouses],
'assignments': assignments
}
return {'status': LpStatus[prob.status]}
# Example
customer_locs = np.random.rand(40, 2) * 100
potential_wh_locs = np.random.rand(15, 2) * 100
service_radius = 25
result = coverage_based_warehouse_location(customer_locs, service_radius,
potential_wh_locs)
print(f"Minimum warehouses for coverage: {result['num_warehouses']}")
print(f"Warehouse indices: {result['open_warehouses']}")
ヒューリスティック手法
1. 貪欲な開設ヒューリスティック
def greedy_warehouse_opening(warehouse_costs, customer_demands,
transport_costs, capacities):
"""
Greedy heuristic: open warehouses one at a time
Select warehouse that gives maximum cost reduction
Args:
warehouse_costs: fixed costs for each warehouse
customer_demands: customer demands
transport_costs: matrix [warehouses x customers]
capacities: warehouse capacities
Returns:
heuristic solution
"""
n_warehouses = len(warehouse_costs)
n_customers = len(customer_demands)
open_warehouses = []
unserved_customers = set(range(n_customers))
def calculate_total_cost(open_whs, assignments):
"""Calculate total cost for given configuration"""
fixed = sum(warehouse_costs[i] for i in open_whs)
transport = sum(
transport_costs[assignments[j]][j] * customer_demands[j]
for j in range(n_customers) if j in assignments
)
return fixed + transport
# Iteratively open warehouses
while unserved_customers:
best_warehouse = None
best_cost = float('inf')
best_assignments = None
# Try opening each unopened warehouse
for wh in range(n_warehouses):
if wh in open_warehouses:
continue
test_warehouses = open_warehouses + [wh]
# Assign customers greedily to nearest warehouse
test_assignments = {}
remaining_capacity = {w: capacities[w] for w in test_warehouses}
# Sort customers by closest distance to this new warehouse
customer_distances = [
(transport_costs[wh][j], j) for j in unserved_customers
]
customer_distances.sort()
for dist, cust in customer_distances:
# Assign to nearest warehouse with capacity
assigned = False
for w in sorted(test_warehouses,
key=lambda x: transport_costs[x][cust]):
if remaining_capacity[w] >= customer_demands[cust]:
test_assignments[cust] = w
remaining_capacity[w] -= customer_demands[cust]
assigned = True
break
if not assigned:
test_assignments[cust] = min(test_warehouses,
key=lambda x: transport_costs[x][cust])
# Calculate cost
test_cost = calculate_total_cost(test_warehouses, test_assignments)
if test_cost < best_cost:
best_cost = test_cost
best_warehouse = wh
best_assignments = test_assignments
# Open best warehouse
if best_warehouse is not None:
open_warehouses.append(best_warehouse)
unserved_customers = {c for c in unserved_customers
if c not in best_assignments}
# Update assignments for served customers
if not unserved_customers:
assignments = best_assignments
break
else:
break
total_cost = calculate_total_cost(open_warehouses, assignments)
return {
'open_warehouses': open_warehouses,
'assignments': assignments,
'total_cost': total_cost,
'method': 'Greedy Opening'
}
2. 削減額に基づく統合
def savings_based_consolidation(warehouse_costs, customer_demands,
transport_costs, initial_warehouses=None):
"""
Savings-based heuristic for warehouse consolidation
Start with many warehouses, consolidate based on savings
Args:
warehouse_costs: fixed costs
customer_demands: demands
transport_costs: transport cost matrix
initial_warehouses: starting warehouse set
Returns:
consolidated solution
"""
n_warehouses = len(warehouse_costs)
n_customers = len(customer_demands)
# Start with all warehouses if not specified
if initial_warehouses is None:
open_warehouses = set(range(n_warehouses))
else:
open_warehouses = set(initial_warehouses)
def assign_customers(whs):
"""Assign each customer to nearest warehouse"""
assignments = {}
for j in range(n_customers):
nearest = min(whs, key=lambda i: transport_costs[i][j])
assignments[j] = nearest
return assignments
def calculate_cost(whs, assignments):
"""Calculate total cost"""
fixed = sum(warehouse_costs[i] for i in whs)
transport = sum(
transport_costs[assignments[j]][j] * customer_demands[j]
for j in range(n_customers)
)
return fixed + transport
improved = True
while improved and len(open_warehouses) > 1:
improved = False
current_assignments = assign_customers(open_warehouses)
current_cost = calculate_cost(open_warehouses, current_assignments)
# Try closing each warehouse
best_warehouse_to_close = None
best_cost_after_closing = current_cost
for wh in list(open_warehouses):
# Test closing this warehouse
test_warehouses = open_warehouses - {wh}
test_assignments = assign_customers(test_warehouses)
test_cost = calculate_cost(test_warehouses, test_assignments)
# Check if closing reduces cost
if test_cost < best_cost_after_closing:
best_cost_after_closing = test_cost
best_warehouse_to_close = wh
improved = True
# Close best warehouse if improvement found
if improved:
open_warehouses.remove(best_warehouse_to_close)
final_assignments = assign_customers(open_warehouses)
final_cost = calculate_cost(open_warehouses, final_assignments)
return {
'open_warehouses': list(open_warehouses),
'assignments': final_assignments,
'total_cost': final_cost,
'method': 'Savings-Based Consolidation'
}
倉庫立地の完全な最適化問題
class WarehouseLocationSolver:
"""
Comprehensive warehouse location optimization solver
"""
def __init__(self):
self.warehouse_data = None
self.customer_data = None
self.transport_costs = None
self.solution = None
def load_problem(self, warehouse_data, customer_data,
transport_costs=None, transport_rate=None):
"""
Load problem data
Args:
warehouse_data: DataFrame with warehouse info
customer_data: DataFrame with customer info
transport_costs: cost matrix or None
transport_rate: if transport_costs None, calculate from rate
"""
self.warehouse_data = warehouse_data
self.customer_data = customer_data
if transport_costs is not None:
self.transport_costs = transport_costs
elif transport_rate is not None:
# Calculate costs from distances
self.transport_costs = self._calculate_transport_costs(transport_rate)
else:
raise ValueError("Must provide transport_costs or transport_rate")
print(f"Loaded warehouse location problem:")
print(f" Potential warehouses: {len(warehouse_data)}")
print(f" Customers: {len(customer_data)}")
print(f" Total demand: {customer_data['demand'].sum():,.0f}")
def _calculate_transport_costs(self, rate):
"""Calculate transport costs from coordinates and rate"""
costs = np.zeros((len(self.warehouse_data), len(self.customer_data)))
for i in self.warehouse_data.index:
for j in self.customer_data.index:
dist = np.sqrt(
(self.warehouse_data.loc[i, 'lat'] -
self.customer_data.loc[j, 'lat'])**2 +
(self.warehouse_data.loc[i, 'lon'] -
self.customer_data.loc[j, 'lon'])**2
)
costs[i][j] = dist * rate
return costs
def solve_exact(self, time_limit=600):
"""Solve with MIP (exact)"""
print("\nSolving with MIP (exact method)...")
return solve_warehouse_location(
self.warehouse_data,
self.customer_data,
self.transport_costs
)
def solve_heuristic(self, method='greedy'):
"""
Solve with heuristic
Args:
method: 'greedy', 'gravity', 'coverage', 'savings'
"""
print(f"\nSolving with {method} heuristic...")
if method == 'greedy':
return greedy_warehouse_opening(
self.warehouse_data['fixed_cost'].values,
self.customer_data['demand'].values,
self.transport_costs,
self.warehouse_data['capacity'].values
)
elif method == 'gravity':
customer_locs = self.customer_data[['lat', 'lon']].values
customer_dems = self.customer_data['demand'].values
# Estimate number of warehouses from capacity
total_demand = customer_dems.sum()
avg_capacity = self.warehouse_data['capacity'].mean()
num_whs = max(1, int(np.ceil(total_demand / avg_capacity * 1.2)))
locations = gravity_location_model(customer_locs, customer_dems,
num_warehouses=num_whs)
# Match to nearest potential warehouses
open_whs = []
for loc in locations:
nearest = None
min_dist = float('inf')
for i in self.warehouse_data.index:
wh_loc = self.warehouse_data.loc[i, ['lat', 'lon']].values
dist = np.linalg.norm(loc - wh_loc)
if dist < min_dist:
min_dist = dist
nearest = i
open_whs.append(nearest)
return {'open_warehouses': open_whs, 'method': 'Gravity'}
else:
raise ValueError(f"Unknown method: {method}")
def compare_solutions(self, methods=['greedy', 'savings', 'exact']):
"""Compare multiple solution methods"""
import pandas as pd
import time
results = []
for method in methods:
start_time = time.time()
try:
if method == 'exact':
solution = self.solve_exact()
else:
solution = self.solve_heuristic(method)
solve_time = time.time() - start_time
results.append({
'Method': method,
'Total Cost': solution['total_cost'],
'Warehouses': len(solution['open_warehouses']),
'Time (s)': f"{solve_time:.2f}"
})
except Exception as e:
print(f"Error with {method}: {e}")
df = pd.DataFrame(results)
if len(df) > 0:
best_cost = df['Total Cost'].min()
df['Gap %'] = ((df['Total Cost'] - best_cost) / best_cost * 100).round(2)
return df
def visualize_network(self, solution, title="Warehouse Network"):
"""Visualize warehouse network"""
import matplotlib.pyplot as plt
plt.figure(figsize=(14, 10))
# Plot customers (blue circles)
plt.scatter(self.customer_data['lon'],
self.customer_data['lat'],
c='lightblue', s=50, alpha=0.6,
label='Customers')
# Plot all potential warehouses (gray)
plt.scatter(self.warehouse_data['lon'],
self.warehouse_data['lat'],
c='lightgray', s=200, alpha=0.3,
marker='s', label='Potential Warehouses')
# Plot open warehouses (red)
open_wh_data = self.warehouse_data.loc[solution['open_warehouses']]
plt.scatter(open_wh_data['lon'],
open_wh_data['lat'],
c='red', s=300, alpha=0.8,
marker='s', label='Open Warehouses',
edgecolors='black', linewidths=2)
# Draw assignments
if 'assignments' in solution:
for cust_id, assignment in solution['assignments'].items():
if isinstance(assignment, list):
wh_id = assignment[0]['warehouse']
else:
wh_id = assignment
cust_loc = self.customer_data.loc[cust_id, ['lon', 'lat']].values
wh_loc = self.warehouse_data.loc[wh_id, ['lon', 'lat']].values
plt.plot([cust_loc[0], wh_loc[0]],
[cust_loc[1], wh_loc[1]],
'k-', alpha=0.1, linewidth=0.5)
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.title(title)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Complete example
if __name__ == "__main__":
print("="*70)
print("WAREHOUSE LOCATION OPTIMIZATION - COMPLETE EXAMPLE")
print("="*70)
np.random.seed(42)
# Generate problem data
n_warehouses = 10
n_customers = 50
warehouse_df = pd.DataFrame({
'warehouse_id': range(n_warehouses),
'fixed_cost': np.random.uniform(400000, 700000, n_warehouses),
'capacity': np.random.uniform(5000, 15000, n_warehouses),
'lat': np.random.uniform(30, 45, n_warehouses),
'lon': np.random.uniform(-120, -70, n_warehouses)
})
warehouse_df.index = warehouse_df['warehouse_id']
customer_df = pd.DataFrame({
'customer_id': range(n_customers),
'demand': np.random.uniform(100, 800, n_customers),
'lat': np.random.uniform(30, 45, n_customers),
'lon': np.random.uniform(-120, -70, n_customers)
})
customer_df.index = customer_df['customer_id']
# Create solver
solver = WarehouseLocationSolver()
solver.load_problem(warehouse_df, customer_df, transport_rate=0.5)
# Compare solutions
print("\n" + "="*70)
print("COMPARING SOLUTION METHODS")
print("="*70)
comparison = solver.compare_solutions(['greedy', 'exact'])
print("\n" + comparison.to_string(index=False))
# Detailed solution
print("\n" + "="*70)
print("DETAILED OPTIMAL SOLUTION")
print("="*70)
best_solution = solver.solve_exact()
print(f"Total Cost: ${best_solution['total_cost']:,.2f}")
print(f" Fixed: ${best_solution['fixed_cost']:,.2f} "
f"({best_solution['fixed_cost']/best_solution['total_cost']*100:.1f}%)")
print(f" Transport: ${best_solution['transport_cost']:,.2f} "
f"({best_solution['transport_cost']/best_solution['total_cost']*100:.1f}%)")
print(f"\nWarehouses Opened: {best_solution['num_warehouses']}")
for wh_id in best_solution['open_warehouses']:
util = best_solution['utilization'][wh_id]
cap = warehouse_df.loc[wh_id, 'capacity']
cost = warehouse_df.loc[wh_id, 'fixed_cost']
print(f" Warehouse {wh_id}: Util={util:.1f}%, "
f"Cap={cap:,.0f}, Cost=${cost:,.0f}")
# Visualize
solver.visualize_network(best_solution,
title="Optimal Warehouse Network")
ツールとライブラリ
Python ライブラリ
最適化:
- PuLP:MIP モデリング
- Pyomo:高度な最適化
- OR-Tools:Google の最適化ツール
- Gurobi/CPLEX:商用ソルバー
データと分析:
- pandas:データ操作
- numpy:数値計算
- scikit-learn:クラスタリング、ML
地理空間:
- geopy:距離計算
- folium:インタラクティブマップ
- geopandas:地理空間データ
可視化:
- matplotlib:プロット作成
- plotly:インタラクティブな可視化
- seaborn:統計的な可視化
商用ソフトウェア
- Llamasoft (Coupa):サプライチェーンネットワーク設計
- LLamasoft Design:倉庫最適化
- SAP IBP:統合ビジネスプランニング
- Blue Yonder:サプライチェーンプラットフォーム
- o9 Solutions:計画プラットフォーム
よくある課題と解決策
課題:多段階ネットワーク
問題:
- 工場 → 地域 DC → 地域倉庫 → 顧客
- 複雑なフローのパターン
- 複数の意思決定レイヤー
解決策:
- 階層的な分解
- 多段階最適化
- 実行可能であれば同時最適化
- 反復的な改善
課題:季節的な需要
問題:
- 季節によって需要が大きく変動する
- 倉庫に関する意思決定は長期的
- ピーク時の必要処理能力
解決策:
- ピークシーズンを明示的にモデル化する
- 柔軟性/増強能力を含める
- ピーク時に一時的な倉庫を利用する
- サードパーティ・ロジスティクス(3PL)の選択肢を検討する
- 複数期間モデルを使用する
課題:在庫に関する考慮事項
問題:
- 倉庫の立地が在庫レベルに影響する
- 倉庫が増えるほど、安全在庫も増える
- 単純なモデルではトレードオフが捉えられない
解決策:
- モデルに在庫コストを含める
- 安全在庫には平方根則を適用する
- 集約によるリスクプーリングのメリットを活用する
- 多段階在庫最適化を行う
課題:現実世界の制約
問題:
- ゾーニング規制
- 労働力の確保可能性
- 不動産の確保可能性
- インフラ(港湾、鉄道、高速道路)
- 税制優遇措置
解決策:
- 制約条件としてモデルに含める
- シナリオ分析を行う
- 最適化後に実現可能性を確認する
- 立地選定の専門家と連携する
出力形式
倉庫立地最適化ソリューションのレポート
問題インスタンス:
- 候補倉庫:15
- 顧客:100
- 年間総需要:125,000 単位
- 計画期間:5 年
最適なネットワーク構成:
| 指標 | 値 |
|---|---|
| 年間総コスト | $8,247,500 |
| 固定費 | $2,450,000 (29.7%) |
| 出荷輸送費 | $4,823,000 (58.5%) |
| 入荷輸送費 | $974,500 (11.8%) |
| 開設した倉庫 | 4 |
| 平均稼働率 | 78.3% |
| サービス対象率 | 100% |
開設する倉庫:
| WH ID | 立地 | 固定費 | 処理能力 | 稼働率 | 対応顧客数 |
|---|---|---|---|---|---|
| 3 | Dallas, TX | $650,000 | 35,000 | 82.1% | 28 |
| 7 | Atlanta, GA | $580,000 | 30,000 | 76.8% | 24 |
| 11 | Los Angeles, CA | $720,000 | 40,000 | 81.4% | 31 |
| 14 | Chicago, IL | $500,000 | 25,000 | 72.9% | 17 |
ネットワーク統計:
- 顧客までの平均距離:287 マイル
- 顧客までの最大距離:612 マイル
- 単位あたりの平均出荷輸送費:$38.58
- 処理能力の余裕:21.7%(成長/季節変動に備えるため)
確認すべき質問
- グリーンフィールド(新規ネットワーク)分析ですか、それともブラウンフィールド(既存ネットワーク)分析ですか?
- 倉庫の候補地はいくつありますか?
- 顧客は何社ありますか?所在地と需要量はどのようになっていますか?
- 倉庫の固定費(リース料/建設費)はいくらですか?
- 運営費(人件費、光熱費、管理費)はいくらですか?
- 倉庫の容量、または容量の選択肢はどのようになっていますか?
- 輸送費はいくらですか?(料金または距離ベース)
- サービス要件はありますか?(配送時間、カバー距離)
- 維持する必要がある既存施設はありますか?
- 計画期間はどのくらいですか?(1年、3年、10年)
- 需要の変動と成長予測はどのようになっていますか?
- 多階層ネットワークですか?(工場、DC、地域倉庫)
- 製品の特性はどのようになっていますか?(価値、重量、保管要件)
- 在庫保有コストは重要ですか?
関連スキル
- facility-location-problem:一般的な施設配置理論
- distribution-center-network:DC 固有のネットワーク設計
- network-design:サプライチェーンのエンドツーエンドネットワーク
- hub-location-problem:ハブ・アンド・スポークネットワーク
- set-covering-problem:カバレッジベースの配置
- inventory-optimization:在庫と配置のトレードオフ
- network-flow-optimization:ネットワーク内のフロー配分
- vehicle-routing-problem:倉庫からのラストマイル配送
- multi-echelon-inventory:ネットワークの各階層における在庫
コメント
Markdown 対応