python的运筹学工业场景模拟第七十四篇:解析外协加工报价,区分外协最大产能,构建自产+外协混合生产模型全部约束。

📅 2026/8/20 16:52:55
python的运筹学工业场景模拟第七十四篇:解析外协加工报价,区分外协最大产能,构建自产+外协混合生产模型全部约束。
自产外协“调度员”用Python把报价单转成约束让PuLP算清“自己干还是外协干”“某机械加工厂有 20 台 CNC、15 台车床但每月总有 30% 订单要外协。以前计划员拿着外协报价单和产能表手工算‘哪些自己干、哪些外发’一算就是 4 小时还经常算错。上个月外协厂涨价计划员没及时算多花了 5.2 万元。后来我写了个外协报价解析混合生产建模程序0.6 秒解析 12 家外协报价自动生成 48 条产能与成本约束PuLP 直接给出最优方案。厂长说‘这 0.6 秒把 4 小时和 5 万成本一起省了。’”—— 参考北京理工大学《运筹学》第 3 章“运输问题”、第 6 章“整数规划应用”一、实际应用场景描述外协报价解析 → 自产外协混合生产建模器是产能不足、需要外协补充的制造企业的核心决策工具。凡是“自己干不完、要找外协”的地方都是它行业 自产资源 外协场景 决策难点机械加工 CNC、车床、铣床 电镀、热处理、焊接 外协价格 vs 自产成本电子制造 SMT、测试、组装 注塑、模具、包装 外协交期 vs 自产产能汽车零部件 冲压、焊接、机加 表面处理、铸造 外协质量 vs 自产一致性医疗器械 机加、清洗、组装 灭菌、检测、包装 合规要求 vs 外协资质航空航天 精密加工、装配 特种工艺、检测 技术保密 vs 外协能力五金制品 冲压、抛光、装配 电镀、喷涂、丝印 环保限制 vs 外协合规核心矛盾- 计划员有外协报价单Excel/PDF/ERP导出但不知道怎么转成数学模型- 自产有产能上限、单位成本外协有报价、最大承接量、交期- PuLP 需要知道“谁干多少、花多少钱”但手工建模容易漏约束、算错成本。┌──────────────────────────────────────────────────────────────┐│ 外协报价解析 → 混合生产建模器 · 自产外协调度员 ││ ││ 【业务场景】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 输入: │││ │ • 自产: 设备清单、产能、单位成本 │││ │ • 外协: 报价单(12家外协厂, 不同工艺不同价格) │││ │ • 订单: 产品需求、交期要求 │││ │ │││ │ 处理管道: │││ │ 1. 解析: 读取外协报价单, 提取工艺-价格映射 │││ │ 2. 建模: 构建自产外协混合生产模型 │││ │ 3. 约束: 自产产能、外协最大承接量、订单需求 │││ │ 4. 求解: PuLP优化总成本 │││ │ │││ │ 输出: │││ │ • 最优生产方案(自产多少、外协多少) │││ │ • 总成本、外协占比、产能利用率 │││ │ • 外协厂分配明细 │││ └─────────────────────────────────────────────────────────┘││ ││ 【核心矛盾】 ││ • 计划员: 想算清自己干还是外协干 │││ • 外协报价: 分散在Excel/PDF, 难统一 │││ • 本程序: 把报价单转成约束 — 自产外协调度员 │││ ││ 【本程序处理流程】 ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐││ │ 解析报价 │──►│ 构建模型 │──►│ 生成约束 │──►│ 优化求解 │││ │ 单 │ │ (自产外 │ │ (产能成 │ │ (最小总 │││ │ │ │ 协) │ │ 本) │ │ 成本) │││ └──────────┘ └──────────┘ └──────────┘ └──────────┘│└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某机械加工厂计划员原话“我们厂有 20 台 CNC、15 台车床、10 台铣床主要做精密零件加工。每月订单约 120 个但自产产能只能满足 70%剩下 30% 必须外协。外协厂有 12 家每家报价不一样- A厂CNC加工 85 元/小时车床 65 元/小时但每月最多接 200 小时- B厂CNC加工 92 元/小时车床 70 元/小时最多接 300 小时- C厂只做电镀15 元/件最多 5000 件- ……我得拿着 12 份报价单、自产产能表、订单需求表手工算1. 哪些订单自产成本低、产能够2. 哪些订单外协产能不够、外协便宜3. 外协给哪家比价、看承接量一算就是 4 小时还经常算错。上个月C厂电镀涨价 20%我按旧价格算把 2000 件电镀件全给了 C 厂多花了 5.2 万元。后来 IT 组写了个 Python 脚本——0.6 秒解析 12 家外协报价自动生成 48 条产能与成本约束PuLP 直接给出最优方案。厂长说‘这 0.6 秒把 4 小时和 5 万成本一起省了。’”2.2 人工计算 vs 自动建模量化对比指标 人工计算 Python 自动建模本方案 改善效果12家外协报价解析 30 分钟 0.1 秒 -99.94%混合生产建模 4 小时 0.5 秒 -99.97%建模错误率 ~15%漏约束、算错成本 0% 消除方案优化度 凭经验非最优 数学最优 质变隐性成本 5.2 万元/月报价更新不及时 0 元 消除计划员加班 每周 8 小时 0 小时 消除关键发现自产外协混合生产的核心难点不在“求解”而在“建模”。一旦外协报价能自动解析成约束求解就是“顺便的事”。三、核心逻辑讲解大白话版3.1 用大白话解释“自产外协混合生产”想象你要办一场 100 人的聚餐- 自家厨房能炒 60 人份的菜成本 20 元/人- 外面餐馆能炒 50 人份但每家价格不一样- A 餐馆30 元/人最多接 30 人- B 餐馆35 元/人最多接 40 人- C 餐馆40 元/人最多接 20 人。你要决定- 自家厨房炒多少60 人以内尽量自己炒便宜- 外面餐馆订多少不够的找外面- 外面订哪家比价选便宜的但不能超过每家的最大承接量大白话约束1. 总人数要够自家 外面 100 人2. 自家不超产能自家最多炒 60 人3. 外协不超承接量A 餐馆最多 30 人B 最多 40 人4. 目标总成本最低。工业现场版- 自家厨房 自产产能- 外面餐馆 外协厂- 每人份成本 单位加工成本- 最大承接量 外协最大产能- 总人数 订单总需求3.2 运筹学模型北理工《运筹学》映射参考北理工《运筹学》第 3 章“运输问题”、第 6 章“整数规划应用”自产外协混合生产模型线性规划\begin{aligned}\min \quad Z \sum_{i1}^{n} c_i x_i \sum_{j1}^{m} \sum_{k1}^{K} p_{jk} y_{jk} \\\text{s.t.} \quad \sum_{i1}^{n} x_i \sum_{j1}^{m} \sum_{k1}^{K} y_{jk} \ge D_k, \quad k1,\dots,K \quad \text{(需求满足)} \\ x_i \le C_i, \quad i1,\dots,n \quad \text{(自产产能)} \\ \sum_{k1}^{K} y_{jk} \le Q_j, \quad j1,\dots,m \quad \text{(外协最大承接量)} \\ x_i \ge 0, \quad y_{jk} \ge 0\end{aligned}变量说明- x_i 自产资源 i 的生产量- y_{jk} 外协厂 j 生产产品 k 的数量- c_i 自产单位成本- p_{jk} 外协厂 j 加工产品 k 的报价- C_i 自产资源 i 的产能上限- Q_j 外协厂 j 的最大承接量- D_k 产品 k 的需求量。北理工教材要点- 第 3 章 §3.1运输问题是多源供应、多需求分配的经典模型- 第 3 章 §3.2运输表的标准化表示- 第 6 章 §6.3生产计划问题中自产与外协的权衡- 本程序解决的是“外协报价解析 混合生产约束自动生成”问题。3.3 如何映射到代码中业务逻辑 Python 代码自产资源dataclass InHouseResource外协厂报价dataclass OutsourcingQuote订单需求dataclass ProductionOrder报价解析器QuoteParser.parse_quotes()混合生产建模器HybridProductionModeler.build_model()PuLP 约束生成add_capacity_constraints(),add_outsourcing_constraints()四、OOP 代码实现精简可运行4.1 项目结构hybrid_production_planner/├── hybrid_production_planner.py # 核心代码单文件~350行├── sample_quotes.csv # 示例外协报价单├── sample_orders.csv # 示例订单需求├── sample_inhouse_resources.csv # 示例自产资源├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary外协报价解析 → 自产外协混合生产建模器参考: 北京理工大学《运筹学》第3章运输问题、第6章整数规划应用功能:1. 解析外协报价单(CSV格式)2. 构建自产外协混合生产模型3. 自动生成产能、成本、需求约束4. 调用PuLP求解最小总成本方案5. 输出自产/外协分配明细运行:python hybrid_production_planner.py(需要安装pandas, pulp)import csvimport jsonfrom dataclasses import dataclass, fieldfrom typing import List, Dict, Optional, Tuple, Setfrom enum import Enumimport pulpimport pandas as pdimport time# ─── 枚举与常量 ────────────────────────────────────────────────────────────class ProcessType(Enum):加工工艺类型CNC CNC加工LATHE 车床加工MILLING 铣床加工GRINDING 磨床加工ELECTROPLATING 电镀HEAT_TREATMENT 热处理WELDING 焊接ASSEMBLY 装配PACKAGING 包装class ResourceType(Enum):资源类型INHOUSE 自产OUTSOURCING 外协# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass InHouseResource:自产资源resource_id: strresource_name: strprocess_type: ProcessTypecapacity: float # 产能(小时/月 或 件/月)unit_cost: float # 单位成本(元/小时 或 元/件)capacity_unit: str 小时available_hours: float 720.0 # 每月可用工时(默认24天×30小时)propertydef monthly_capacity(self) - float:月产能if self.capacity_unit 小时:return self.capacity * self.available_hoursreturn self.capacitydef __str__(self):return f{self.resource_name}({self.process_type.value}): {self.monthly_capacity}{self.capacity_unit}, {self.unit_cost}元/{self.capacity_unit}dataclassclass OutsourcingQuote:外协报价vendor_id: strvendor_name: strprocess_type: ProcessTypeunit_price: float # 单价(元/小时 或 元/件)max_capacity: float # 最大承接量capacity_unit: str 小时lead_time_days: int 7 # 交期(天)quality_rating: float 0.95 # 质量评级(0-1)is_qualified: bool True # 是否合格供应商propertydef effective_price(self) - float:有效价格(考虑质量折扣)return self.unit_price / max(self.quality_rating, 0.8)def __str__(self):return f{self.vendor_name}({self.process_type.value}): {self.unit_price}元/{self.capacity_unit}, 最大{self.max_capacity}{self.capacity_unit}dataclassclass ProductionOrder:生产订单order_id: strproduct_name: strprocess_requirements: Dict[ProcessType, float] # 工艺→需求量due_date: strpriority: int 1 # 1-5, 5最高propertydef total_processing_hours(self) - float:总加工工时return sum(qty for qty in self.process_requirements.values() if isinstance(qty, (int, float)))def __str__(self):processes , .join([f{k.value}:{v} for k, v in self.process_requirements.items()])return f{self.order_id}({self.product_name}): {processes}, 交期:{self.due_date}dataclassclass ProductionPlan:生产计划结果order_id: strinhouse_allocation: Dict[str, float] # 自产资源ID→分配量outsourcing_allocation: Dict[str, float] # 外协厂ID→分配量total_cost: floatinhouse_cost: floatoutsourcing_cost: floatcompletion_rate: float # 完成率(0-1)def summary(self) - str:return (f订单{self.order_id}: 总成本{self.total_cost:.2f}元, f自产{self.inhouse_cost:.2f}元, 外协{self.outsourcing_cost:.2f}元, f完成率{self.completion_rate*100:.1f}%)# ─── 报价解析器 ──────────────────────────────────────────────────────────class QuoteParser:外协报价解析器staticmethoddef parse_quotes_from_csv(csv_path: str None) - List[OutsourcingQuote]:从CSV解析外协报价if csv_path is None:return QuoteParser._load_sample_quotes()quotes []try:with open(csv_path, r, encodingutf-8) as f:reader csv.DictReader(f)for row in reader:quote OutsourcingQuote(vendor_idrow[vendor_id],vendor_namerow[vendor_name],process_typeProcessType(row[process_type]),unit_pricefloat(row[unit_price]),max_capacityfloat(row[max_capacity]),capacity_unitrow.get(capacity_unit, 小时),lead_time_daysint(row.get(lead_time_days, 7)),quality_ratingfloat(row.get(quality_rating, 0.95)),is_qualifiedrow.get(is_qualified, True) True)quotes.append(quote)except FileNotFoundError:quotes QuoteParser._load_sample_quotes()return quotesstaticmethoddef _load_sample_quotes() - List[OutsourcingQuote]:内置示例外协报价(12家外协厂)quotes [# CNC加工外协OutsourcingQuote(V001, A机械加工厂, ProcessType.CNC, 85, 200, 小时, 5, 0.96),OutsourcingQuote(V002, B精密机械, ProcessType.CNC, 92, 300, 小时, 7, 0.98),OutsourcingQuote(V003, C数控中心, ProcessType.CNC, 78, 150, 小时, 3, 0.94),# 车床加工外协OutsourcingQuote(V004, D车床专营, ProcessType.LATHE, 65, 250, 小时, 4, 0.95),OutsourcingQuote(V005, E车铣复合, ProcessType.LATHE, 70, 180, 小时, 6, 0.97),# 铣床加工外协OutsourcingQuote(V006, F铣床加工, ProcessType.MILLING, 75, 200, 小时, 5, 0.93),OutsourcingQuote(V007, G精密铣削, ProcessType.MILLING, 82, 120, 小时, 8, 0.96),# 电镀外协OutsourcingQuote(V008, H电镀厂, ProcessType.ELECTROPLATING, 15, 5000, 件, 10, 0.98),OutsourcingQuote(V009, I表面处理, ProcessType.ELECTROPLATING, 18, 3000, 件, 12, 0.99),# 热处理外协OutsourcingQuote(V010, J热处理中心, ProcessType.HEAT_TREATMENT, 25, 2000, 件, 7, 0.97),# 焊接外协OutsourcingQuote(V011, K焊接专营, ProcessType.WELDING, 45, 800, 件, 5, 0.94),# 装配外协OutsourcingQuote(V012, L装配车间, ProcessType.ASSEMBLY, 12, 1500, 件, 3, 0.96),]return quotesclass InHouseResourceLoader:自产资源加载器staticmethoddef load_from_csv(csv_path: str None) - List[InHouseResource]:从CSV加载自产资源if csv_path is None:return InHouseResourceLoader._load_sample_resources()resources []try:with open(csv_path, r, encodingutf-8) as f:reader csv.DictReader(f)for row in reader:resource InHouseResource(resource_idrow[resource_id],resource_namerow[resource_name],process_typeProcessType(row[process_type]),capacityfloat(row[capacity]),unit_costfloat(row[unit_cost]),capacity_unitrow.get(capacity_unit, 小时),available_hoursfloat(row.get(available_hours, 720)))resources.append(resource)except FileNotFoundError:resources InHouseResourceLoader._load_sample_resources()return resourcesstaticmethoddef _load_sample_resources() - List[InHouseResource]:内置示例自产资源(20台CNC, 15台车床, 10台铣床)resources []# CNC加工中心for i in range(1, 21):resources.append(InHouseResource(resource_idfCNC{i:02d},resource_namefCNC加工中心{i},process_typeProcessType.CNC,capacity1.0, # 1小时/小时unit_cost55.0, # 自产成本55元/小时capacity_unit小时,available_hours720.0 # 24天×30小时))# 车床for i in range(1, 16):resources.append(InHouseResource(resource_idfLATHE{i:02d},resource_namef车床{i},process_typeProcessType.LATHE,capacity1.0,unit_cost40.0, # 自产成本40元/小时capacity_unit小时,available_hours720.0))# 铣床for i in range(1, 11):resources.append(InHouseResource(resource_idfMILL{i:02d},resource_namef铣床{i},process_typeProcessType.MILLING,capacity1.0,unit_cost50.0, # 自产成本50元/小时capacity_unit小时,available_hours720.0))return resourcesclass OrderLoader:订单加载器staticmethoddef load_from_csv(csv_path: str None) - List[ProductionOrder]:从CSV加载订单if csv_path is None:return OrderLoader._load_sample_orders()orders []try:with open(csv_path, r, encodingutf-8) as f:reader csv.DictReader(f)for row in reader:# 解析工艺需求(JSON格式)process_reqs {}if row.get(process_requirements):reqs_dict json.loads(row[process_requirements])for proc_name, qty in reqs_dict.items():process_reqs[ProcessType(proc_name)] float(qty)order ProductionOrder(order_idrow[order_id],product_namerow[product_name],process_requirementsprocess_reqs,due_daterow[due_date],priorityint(row.get(priority, 1)))orders.append(order)except FileNotFoundError:orders OrderLoader._load_sample_orders()return ordersstaticmethoddef _load_sample_orders() - List[ProductionOrder]:内置示例订单(120个订单, 总需求约30%外协)orders []# 生成120个订单for i in range(1, 121):# 随机分配工艺需求if i % 3 0:# CNC 车床process_reqs {ProcessType.CNC: 8.0 (i % 5),ProcessType.LATHE: 4.0 (i % 3)}elif i % 3 1:# CNC 铣床process_reqs {ProcessType.CNC: 6.0 (i % 4),ProcessType.MILLING: 3.0 (i % 2)}else:# 车床 铣床 电镀process_reqs {ProcessType.LATHE: 5.0 (i % 4),ProcessType.MILLING: 2.0 (i % 2),ProcessType.ELECTROPLATING: 10.0 (i % 10)}orders.append(ProductionOrder(order_idfORD{i:04d},product_namef零件{i},process_requirementsprocess_reqs,due_datef2024-{(i % 12) 1:02d}-{(i % 28) 1:02d},priority3 (i % 3) # 优先级3-5))return orders# ─── 混合生产建模器 ───────────────────────────────────────────────────────class HybridProductionModeler:自产外协混合生产建模器def __init__(self,inhouse_resources: List[InHouseResource],outsourcing_quotes: List[OutsourcingQuote],orders: List[ProductionOrder]):self.inhouse_resources inhouse_resourcesself.outsourcing_quotes [q for q in outsourcing_quotes if q.is_qualified]self.orders orders# 按工艺类型分组self.inhouse_by_process {}for res in inhouse_resources:if res.process_type not in self.inhouse_by_process:self.inhouse_by_process[res.process_type] []self.inhouse_by_process[res.process_type].append(res)self.outsourcing_by_process {}for quote in self.outsourcing_quotes:if quote.process_type not in self.outsourcing_by_process:self.outsourcing_by_process[quote.process_type] []self.outsourcing_by_process[quote.process_type].append(quote)def build_model(self) - pulp.LpProblem:构建混合生产模型# 创建问题prob pulp.LpProblem(Hybrid_Production_Planning, pulp.LpMinimize)# 决策变量# x[i, k]: 自产资源i生产订单k的量# y[j, k]: 外协厂j生产订单k的量x {}y {}# 为每个订单的每个工艺需求创建变量for order in self.orders:for process_type, demand in order.process_requirements.items():# 自产变量if process_type in self.inhouse_by_process:for res in self.inhouse_by_process[process_type]:var_name fx_{res.resource_id}_{order.order_id}_{process_type.value}x[(res.resource_id, order.order_id, process_type)] pulp.LpVariable(var_name, lowBound0, catContinuous)# 外协变量if process_type in self.outsourcing_by_process:for quote in self.outsourcing_by_process[process_type]:var_name fy_{quote.vendor_id}_{order.order_id}_{process_type.value}y[(quote.vendor_id, order.order_id, process_type)] pulp.LpVariable(var_name, lowBound0, catContinuous)# 目标函数: 最小化总成本total_cost 0# 自产成本for (res_id, order_id, process_type), var in x.items():res next(r for r in self.inhouse_resources if r.resource_id res_id)total_cost res.unit_cost * var# 外协成本for (vendor_id, order_id, process_type), var in y.items():quote next(q for q in self.outsourcing_quotes if q.vendor_id vendor_id)total_cost quote.effective_price * varprob total_cost, Total_Cost# 约束1: 需求满足(每个订单的每个工艺需求必须被满足)for order in self.orders:for process_type, demand in order.process_requirements.items():constraint_expr 0# 自产贡献for res in self.inhouse_by_process.get(process_type, []):key (res.resource_id, order.order_id, process_type)if key in x:constraint_expr x[key]# 外协贡献for quote in self.outsourcing_by_process.get(process_type, []):利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛