关于 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]:由仓库 i 满足的客户 j 对产品 k 的需求比例
- 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:聚类、机器学习
地理空间:
- 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% |
已启用的仓库:
| 仓库 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 年)
- 需求的波动情况和增长预测是什么?
- 是否为多级网络?(工厂、配送中心、本地仓库)
- 产品有哪些特性?(价值、重量、存储要求)
- 库存持有成本是否重要?
相关技能
- facility-location-problem:一般设施选址理论
- distribution-center-network:配送中心专属网络设计
- network-design:端到端供应链网络
- hub-location-problem:枢纽辐射式网络
- set-covering-problem:基于覆盖范围的选址
- inventory-optimization:库存与选址之间的权衡
- network-flow-optimization:网络中的流量分配
- vehicle-routing-problem:从仓库出发的最后一公里配送
- multi-echelon-inventory:跨网络层级的库存
评论
支持 Markdown