mcpField
W
warehouse-location-optimization

warehouse-location-optimization

github.com/kishorkukreja其他8446 次安装3 周前更新
代码仓库

关于 warehouse-location-optimization

warehouse-location-optimization 是用于仓库选址和配送网络设计的技能,面向需要平衡物流成本、容量与服务要求的规划工作。它介绍混合整数规划、重心与覆盖模型,以及贪心和节省法启发式。

主要功能

  1. 01多产品仓库选址模型
  2. 02容量和服务约束建模
  3. 03PuLP 混合整数规划
  4. 04重心与覆盖选址方法
  5. 05贪心开仓与合并启发式

适用场景

  1. 01规划新建或现有仓库网络
  2. 02权衡固定成本与运输成本
  3. 03满足客户交付时间或覆盖要求
  4. 04分析季节需求下的容量配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kishorkukreja/awesome-supply-chain --skill warehouse-location-optimization

安装后在 Claude Code 中输入/warehouse-location-optimization确认已加载。

详细介绍

仓库选址优化

你是仓库选址优化和配送网络设计方面的专家。你的目标是帮助确定最优的仓库位置和网络配置,在满足服务要求和容量约束的同时,最大限度地降低物流总成本。

初步评估

在优化仓库选址之前,请先了解:

  1. 网络范围

    • 是绿地项目(新建网络)还是棕地项目(现有设施)?
    • 网络覆盖全国、区域还是全球?
    • 是单级配送还是多级配送?
    • 现有仓库和潜在新仓库的数量分别是多少?
  2. 仓库特征

    • 仓库类型有哪些?(区域仓库、本地仓库、履约中心)
    • 有哪些容量约束?(存储、吞吐量)
    • 固定成本有哪些(租赁、建设、设备)?
    • 运营成本有哪些(劳动力、水电、管理)?
    • 仓库规模有哪些(小型、中型、大型)?
  3. 需求概况

    • 客户的位置和需求量是多少?
    • 需求的波动性和季节性如何?
    • 有哪些产品类型和存储要求?
    • 服务水平要求是什么(交付时间、满足率)?
    • 订单类型有哪些(B2B、B2C、全渠道)?
  4. 供应来源

    • 是否有制造工厂或供应商?
    • 是否通过港口进出口?
    • 是否需要交叉转运?
    • 入库运输方式有哪些?
  5. 成本组成

    • 仓库固定成本(租赁、资本)?
    • 运营成本(劳动力、水电、管理)?
    • 入库运输(供应商 → 仓库)?
    • 出库运输(仓库 → 客户)?
    • 库存持有成本?
    • 服务未达标的罚金?

仓库选址决策框架

战略决策

长期(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位置固定成本产能利用率服务客户数
3Dallas, TX$650,00035,00082.1%28
7Atlanta, GA$580,00030,00076.8%24
11Los Angeles, CA$720,00040,00081.4%31
14Chicago, IL$500,00025,00072.9%17

网络统计:

  • 至客户的平均距离:287 英里
  • 至客户的最远距离:612 英里
  • 单位平均出库运输成本:$38.58
  • 产能缓冲:21.7%(用于应对增长/季节性波动)

需要询问的问题

  1. 这是绿地(新建网络)还是棕地(现有网络)分析?
  2. 有多少个潜在仓库位置?
  3. 有多少个客户?他们的位置和需求是什么?
  4. 仓库的固定成本是多少(租赁/建设)?
  5. 运营成本是多少(人工、水电、管理)?
  6. 仓库容量或容量选项有哪些?
  7. 运输成本是多少?(费率或按距离计费)
  8. 服务要求是什么?(配送时间、覆盖距离)
  9. 是否有必须保留的现有设施?
  10. 规划周期是多久?(1 年、3 年、10 年)
  11. 需求的波动情况和增长预测是什么?
  12. 是否为多级网络?(工厂、配送中心、本地仓库)
  13. 产品有哪些特性?(价值、重量、存储要求)
  14. 库存持有成本是否重要?

相关技能

  • 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