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]:顧客 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立地固定費処理能力稼働率対応顧客数
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. 多階層ネットワークですか?(工場、DC、地域倉庫)
  13. 製品の特性はどのようになっていますか?(価値、重量、保管要件)
  14. 在庫保有コストは重要ですか?

関連スキル

  • 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:ネットワークの各階層における在庫

コメント