174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
import pandas as pd
|
||
import pulp
|
||
from math import radians, sin, cos, sqrt, atan2
|
||
|
||
# 1. 读取数据
|
||
inventory_df = pd.read_excel('可调拨库存表.xlsx')
|
||
factory_loc_df = pd.read_excel('工厂位置.xlsx')
|
||
demand_df = pd.read_excel('需求表.xlsx') # 型号、市、需求
|
||
city_loc_df = pd.read_excel('地名经纬度.xlsx') # 市、经度、纬度
|
||
|
||
# 2. 数据合并
|
||
inventory_df['可调拨库存'] = pd.to_numeric(inventory_df['可调拨库存'], errors='coerce').fillna(0)
|
||
demand_df['需求'] = pd.to_numeric(demand_df['需求'], errors='coerce').fillna(0)
|
||
|
||
# 工厂库存合并
|
||
factory_capacity = {}
|
||
for _, row in inventory_df.groupby(['工厂', '型号'])['可调拨库存'].sum().reset_index().iterrows():
|
||
f, p, cap = row['工厂'], row['型号'], int(row['可调拨库存'])
|
||
if cap > 0:
|
||
factory_capacity.setdefault(f, {})[p] = cap
|
||
|
||
# 城市需求合并
|
||
city_demand = {}
|
||
for _, row in demand_df.iterrows():
|
||
city, p, dem = row['市'], row['型号'], int(row['需求'])
|
||
if dem > 0:
|
||
city_demand.setdefault(city, {})
|
||
city_demand[city][p] = city_demand[city].get(p, 0) + dem
|
||
|
||
factory_lonlat = dict(zip(factory_loc_df['工厂'], zip(factory_loc_df['经度'], factory_loc_df['纬度'])))
|
||
city_lonlat = dict(zip(city_loc_df['市'], zip(city_loc_df['经度'], city_loc_df['纬度'])))
|
||
|
||
# missing = set(city_demand) - set(city_lonlat)
|
||
# if missing:
|
||
# raise ValueError(f"缺少经纬度:{missing}")
|
||
|
||
# 3. 距离
|
||
dist = {}
|
||
for f in factory_capacity:
|
||
for c in city_demand:
|
||
lon1, lat1 = factory_lonlat[f]
|
||
lon2, lat2 = city_lonlat[c]
|
||
dlon = radians(lon2-lon1)
|
||
dlat = radians(lat2-lat1)
|
||
a = sin(dlat/2)**2 + cos(radians(lat1))*cos(radians(lat2))*sin(dlon/2)**2
|
||
dist[(f,c)] = 6371 * 2 * atan2(sqrt(a), sqrt(1-a))
|
||
|
||
# 4. 单位运费函数
|
||
def get_real_unit_cost(distance_km, product):
|
||
d = distance_km
|
||
p = str(product).strip()
|
||
if p in ['GWBD-A2', 'GWBD-A3', 'GWBD-B']:
|
||
if distance_km <= 100:
|
||
return 15802.31
|
||
elif distance_km <= 200:
|
||
return 18445.12
|
||
elif distance_km <= 300:
|
||
return 22391.35
|
||
elif distance_km <= 600:
|
||
return 62.29 * d
|
||
elif distance_km <= 900:
|
||
return 44.75 * d
|
||
elif distance_km <= 1200:
|
||
return 38.23 * d
|
||
elif distance_km <= 1500:
|
||
return 36.08 * d
|
||
elif distance_km <= 2000:
|
||
return 33.44 * d
|
||
elif distance_km <= 2500:
|
||
return 32.76 * d
|
||
elif distance_km <= 3500:
|
||
return 26.68 * d
|
||
else:
|
||
return 24.27
|
||
# 其他型号用第二套
|
||
else:
|
||
if distance_km <= 100:
|
||
return 12859.46
|
||
elif distance_km <= 200:
|
||
return 14946.73
|
||
elif distance_km <= 300:
|
||
return 18662.46
|
||
elif distance_km <= 600:
|
||
return 52.83 * d
|
||
elif distance_km <= 900:
|
||
return 38.36 * d
|
||
elif distance_km <= 1200:
|
||
return 32.89 * d
|
||
elif distance_km <= 1500:
|
||
return 30.69 * d
|
||
elif distance_km <= 2000:
|
||
return 28.49 * d
|
||
elif distance_km <= 2500:
|
||
return 27.76 * d
|
||
elif distance_km <= 3500:
|
||
return 22.87 * d
|
||
else:
|
||
return 21.02 * d
|
||
|
||
# 5. 规划模型
|
||
prob = pulp.LpProblem("最小化运费", pulp.LpMinimize)
|
||
|
||
x = {}
|
||
for f in factory_capacity:
|
||
for p in factory_capacity[f]:
|
||
for city in city_demand:
|
||
if city_demand[city].get(p, 0) > 0:
|
||
x[(f,city,p)] = pulp.LpVariable(f"ship_{f}_{city}_{p}", lowBound=0, cat="Integer")
|
||
|
||
# 缺货惩罚
|
||
shortage = {(city,p): pulp.LpVariable(f"short_{city}_{p}", lowBound=0, cat="Continuous")
|
||
for city in city_demand for p in city_demand[city]}
|
||
|
||
BIG_M = 1e10
|
||
prob += pulp.lpSum(
|
||
x[k] * get_real_unit_cost(dist[(k[0],k[1])], k[2]) # 真实单位运费
|
||
for k in x
|
||
) + pulp.lpSum(BIG_M * s for s in shortage.values())
|
||
|
||
# 库存硬约束
|
||
for f in factory_capacity:
|
||
for p in factory_capacity[f]:
|
||
prob += pulp.lpSum(x.get((f,c,p), 0) for c in city_demand) <= factory_capacity[f][p]
|
||
|
||
# 需求软约束
|
||
for city in city_demand:
|
||
for p, dem in city_demand[city].items():
|
||
prob += pulp.lpSum(x.get((f,city,p), 0) for f in factory_capacity) + shortage[(city,p)] >= dem
|
||
|
||
# 6. 求解
|
||
status = prob.solve(pulp.PULP_CBC_CMD(msg=True, timeLimit=600))
|
||
|
||
print(f"\n求解状态: {pulp.LpStatus[status]}")
|
||
if status != 1:
|
||
print("无解(请检查总库存)")
|
||
else:
|
||
print("有解")
|
||
# real_cost = pulp.value(prob.objective)
|
||
# print(f"实际运输总费用: {real_cost:,.2f} 元")
|
||
|
||
# ====================== 7. 输出 ======================
|
||
result = []
|
||
for (f,city,p), var in x.items():
|
||
q = int(pulp.value(var) or 0)
|
||
if q >= 1:
|
||
d = dist[(f,city)]
|
||
# unit = get_real_unit_cost(d, p)
|
||
result.append({
|
||
"工厂": f,
|
||
"型号": p,
|
||
"需求方": city,
|
||
"调拨数量": q,
|
||
# "距离_km": round(d, 1),
|
||
# "真实单位运费": round(unit, 3),
|
||
# "总运费": round(q * unit, 2)
|
||
})
|
||
|
||
df = pd.DataFrame(result)
|
||
if not df.empty:
|
||
df = df.sort_values(["工厂", "型号"]) # 按单位运费排序
|
||
df[["工厂","型号","需求方","调拨数量"]].to_excel("调拨方案.xlsx", index=False)
|
||
# print("\n调拨方案已保存(真实单位运费最优,优先满足单价更低的城市)")
|
||
# print(df[["工厂","型号","需求方","调拨数量"]].to_string(index=False))
|
||
else:
|
||
print("\n无调拨记录")
|
||
|
||
# 校验报告
|
||
# print("\n各城市需求满足情况:")
|
||
# for city in city_demand:
|
||
# for p, dem in city_demand[city].items():
|
||
# sup = sum(r["调拨数量"] for r in result if r["需求方"]==city and r["型号"]==p)
|
||
# print(f" {city} | {p}: 需求 {dem} → 调拨 {sup} → {'完美' if sup==dem else '异常'}")
|