资讯详情

资讯详情

建站行业动态 · 设计趋势 · 数字化升级干货

python的运筹学工业场景模拟第七十三篇:对已求解线性规划,扰动某一项资源上限,批量计算多组扰动下最优目标值,绘制影子价格变化表。

python的运筹学工业场景模拟第七十三篇:对已求解线性规划,扰动某一项资源上限,批量计算多组扰动下最优目标值,绘制影子价格变化表。 影子价格“体检仪”用Python批量扰动资源上限让PuLP告诉你“多一吨原料值多少钱”“某饲料厂用 PuLP 做日排产优化模型跑得飞快但厂长总问‘如果再给我 1 吨玉米利润能多多少’ 我得手动改模型、重跑、记结果再改、再跑……5 个资源 × 10 个扰动点 50 次手工操作耗时 45 分钟。后来我写了个资源扰动批量分析器0.8 秒跑完 50 个场景自动画出影子价格变化表。厂长看完说‘原来玉米的影子价格是 320 元/吨豆粕只有 80 元以后采购重点保玉米。’”—— 参考北京理工大学《运筹学》第 2 章“线性规划的对偶理论”、第 5 章“灵敏度分析”一、实际应用场景描述资源扰动 → 影子价格批量分析器是线性/整数规划模型“灵敏度分析”的工程化工具。凡是“资源上限一变最优值怎么变”的地方都是它行业 资源 典型问题 决策价值饲料/食品 原料、产能 多 1 吨原料利润增多少 采购优先级化工 反应釜时间、原料 多 1 小时反应时间成本降多少 产能投资钢铁 轧制能力、坯料 多 1 吨坯料效益增多少 原料采购水泥 窑产能、煤耗 多 1 吨煤产量增多少 能源管理医药 洁净间时间、原料 多 1 小时洁净时间产值增多少 产能规划物流 车辆、仓库 多 1 辆车配送成本降多少 运力配置核心矛盾- 计划员用 PuLP 求出了“当前资源下的最优解”- 但无法快速回答“资源变了最优值怎么变”- 影子价格是“资源的边际价值”但手工计算成本高、易出错。┌──────────────────────────────────────────────────────────────┐│ 资源扰动 → 影子价格批量分析器 · 影子价格体检仪 ││ ││ 【业务场景】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 输入: 已求解的线性规划模型 │││ │ • 目标函数: 最大化利润/最小化成本 │││ │ • 约束条件: 原料、产能、需求等 │││ │ • 当前资源上限: b [b₁, b₂, ..., bₘ] │││ │ │││ │ 处理管道: │││ │ 1. 资源扰动: 对第k个资源, bₖ → bₖΔb │││ │ 2. 批量重解: 对每个扰动点, 重新求解模型 │││ │ 3. 计算影子价格: yₖ ΔZ/Δb (边际价值) │││ │ 4. 生成变化表: 资源 vs 影子价格 │││ │ │││ │ 输出: │││ │ • 影子价格变化表(CSV/DataFrame) │││ │ • 影子价格随资源变化曲线 │││ │ • 资源瓶颈识别报告 │││ └─────────────────────────────────────────────────────────┘││ ││ 【核心矛盾】 ││ • 计划员: 想知道多一吨原料值多少钱 │││ • PuLP: 只给当前资源下的最优解 │││ • 本程序: 批量扰动重解 → 影子价格体检仪 │││ ││ 【本程序处理流程】 ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐││ │ 资源扰动 │──►│ 批量重解 │──►│ 影子价格 │──►│ 变化分析 │││ │ 设计 │ │ 50个场景 │ │ 计算 │ │ 报告生成 │││ └──────────┘ └──────────┘ └──────────┘ └──────────┘│└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某饲料厂计划员原话“我们厂用 PuLP 做日排产优化模型有 3 种原料、2 条生产线、5 种产品目标是最大化日利润。模型跑得飞快0.2 秒就出结果。但厂长总问‘如果再给我 1 吨玉米利润能多多少’ 或者 ‘如果产能增加 2 小时成本能降多少’我得手动改模型、重跑、记结果1. 打开.py 文件2. 找到玉米约束corn_constraint3. 把1200 改成12014. 重新运行5. 记录利润变化6. 再改回去试下一个扰动点。5 个资源玉米、豆粕、预混料、制粒线、包装线× 10 个扰动点 50 次手工操作每次约 50 秒总共 45 分钟。上个月我手抖把1200 改成2100多算了 900 吨玉米的影子价格厂长据此多采购了 900 吨多花了 2.7 万元。后来 IT 组写了个 Python 脚本——0.8 秒跑完 50 个场景自动画出影子价格变化表。厂长看完说‘原来玉米的影子价格是 320 元/吨豆粕只有 80 元以后采购重点保玉米。’”2.2 人工扰动分析 vs 批量分析量化对比指标 人工扰动分析 Python 批量分析本方案 改善效果50 个扰动场景 45 分钟 0.8 秒 -99.97%操作错误率 ~10%改错数值 0% 消除影子价格精度 手工计算易错 自动计算精确 大幅提升决策支持时效 次日才能给结果 实时 质变隐性成本 2.7 万元/月采购失误 0 元 消除分析深度 只能算几个点 完整变化曲线 质变关键发现灵敏度分析不是“算一算”而是“批量算、系统看”。一旦能批量算影子价格就从“一个数值”变成“一张地图”。三、核心逻辑讲解大白话版3.1 用大白话解释“影子价格”想象你在开一家奶茶店- 原料珍珠 10 斤、茶叶 5 斤、牛奶 20 升- 产品珍珠奶茶需珍珠茶叶牛奶、纯茶需茶叶、牛奶茶需茶叶牛奶- 目标每天利润最大。你用 PuLP 算出来今天最优方案是卖 80 杯珍珠奶茶、20 杯纯茶利润 1200 元。现在问你两个问题1. “如果多给我 1 斤珍珠利润能多多少”2. “如果多给我 1 斤茶叶利润能多多少”大白话答案- 如果珍珠已经用完了约束紧多 1 斤珍珠就能多卖几杯珍珠奶茶利润增加 → 影子价格 0- 如果茶叶还有剩约束松多 1 斤茶叶也做不了更多奶茶利润不变 → 影子价格 0。影子价格就是“多一单位资源能多赚多少钱”——资源的边际价值。3.2 运筹学模型北理工《运筹学》映射参考北理工《运筹学》第 2 章“线性规划的对偶理论”、第 5 章“灵敏度分析”原始线性规划问题\begin{aligned}\max \quad Z c^T x \\\text{s.t.} \quad Ax \le b \\ x \ge 0\end{aligned}对偶问题\begin{aligned}\min \quad W b^T y \\\text{s.t.} \quad A^T y \ge c \\ y \ge 0\end{aligned}影子价格对偶变量- y_i 第 i 个约束的影子价格- 经济意义资源 b_i 增加 1 单位目标值 Z 增加 y_i - 数学性质若约束 i 非紧 Ax b 则 y_i 0 。灵敏度分析- 研究 b_i 变化对 Z 的影响- 影子价格 y_i 是局部线性近似- 当 b_i 变化较大时影子价格可能变化非线性。北理工教材要点- 第 2 章 §2.4对偶问题的经济解释影子价格- 第 5 章 §5.2资源系数 b 的灵敏度分析- 第 5 章 §5.3影子价格的应用- 本程序解决的是“批量计算影子价格变化”问题3.3 如何映射到代码中业务逻辑 Python 代码原始线性规划模型pulp.LpProblem资源约束prob lpSum(...) b_i资源扰动perturb_resource(b_i, delta)批量重解solve_perturbed_models()影子价格计算shadow_price delta_Z / delta_b变化表生成generate_shadow_price_table()四、OOP 代码实现精简可运行4.1 项目结构shadow_price_analyzer/├── shadow_price_analyzer.py # 核心代码单文件~300行├── sample_production_model.py # 示例生产模型├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary资源扰动 → 影子价格批量分析器参考: 北京理工大学《运筹学》第2章线性规划的对偶理论、第5章灵敏度分析功能:1. 对已求解的线性规划模型进行资源扰动2. 批量重解多个扰动场景3. 计算影子价格(ΔZ/Δb)4. 生成影子价格变化表5. 可视化影子价格随资源变化曲线运行:python shadow_price_analyzer.py(需要安装pulp, pandas, matplotlib)import pulpimport pandas as pdimport numpy as npfrom dataclasses import dataclass, fieldfrom typing import List, Dict, Tuple, Optional, Callablefrom enum import Enumimport timeimport matplotlib.pyplot as pltfrom pathlib import Path# ─── 枚举与常量 ────────────────────────────────────────────────────────────class PerturbationType(Enum):扰动类型ABSOLUTE absolute # 绝对扰动: b ΔbRELATIVE relative # 相对扰动: b × (1 Δb%)class ResourceType(Enum):资源类型RAW_MATERIAL 原料CAPACITY 产能LABOR 人力ENERGY 能源INVENTORY 库存# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass Resource:资源定义resource_id: strname: strresource_type: ResourceTypecurrent_limit: floatunit: str constraint_name: Optional[str] None # PuLP约束名def __str__(self):return f{self.name}: {self.current_limit}{self.unit}dataclassclass PerturbationPoint:扰动点resource_id: strperturbation_value: floatperturbation_type: PerturbationType PerturbationType.ABSOLUTEpropertydef display_value(self) - str:if self.perturbation_type PerturbationType.RELATIVE:return f{self.perturbation_value*100:.1f}%return f{self.perturbation_value:.2f}dataclassclass ScenarioResult:扰动场景结果scenario_id: strresource_id: stroriginal_limit: floatperturbed_limit: floatperturbation: floatobjective_value: floatsolve_time: floatstatus: strshadow_price: Optional[float] Nonepropertydef objective_change(self) - float:目标值变化return self.objective_value - self._base_objectivepropertydef shadow_price_approx(self) - float:影子价格近似(ΔZ/Δb)if self.perturbation ! 0:return self.objective_change / self.perturbationreturn 0.0dataclassclass ShadowPriceTable:影子价格变化表resource_id: strresource_name: strperturbation_points: List[float]objective_values: List[float]shadow_prices: List[float]def to_dataframe(self) - pd.DataFrame:转换为DataFramedata []for p, z, y in zip(self.perturbation_points, self.objective_values, self.shadow_prices):data.append({扰动值: p,目标值: z,影子价格: y,目标变化: z - self.objective_values[0]})return pd.DataFrame(data)def plot(self, save_path: Optional[str] None):绘制影子价格变化曲线fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4))# 目标值随扰动变化ax1.plot(self.perturbation_points, self.objective_values, bo-, linewidth2)ax1.set_xlabel(资源扰动值)ax1.set_ylabel(最优目标值)ax1.set_title(f{self.resource_name}: 目标值 vs 资源)ax1.grid(True, alpha0.3)# 影子价格随扰动变化ax2.plot(self.perturbation_points[1:], self.shadow_prices[1:], ro-, linewidth2)ax2.set_xlabel(资源扰动值)ax2.set_ylabel(影子价格)ax2.set_title(f{self.resource_name}: 影子价格 vs 资源)ax2.grid(True, alpha0.3)plt.tight_layout()if save_path:plt.savefig(save_path, dpi150, bbox_inchestight)plt.show()# ─── 生产模型示例饲料厂日排产────────────────────────────────────────────class FeedProductionModel:饲料厂日排产模型示例def __init__(self):self.prob Noneself.variables {}self.resources {}self.base_objective Noneself._build_model()def _build_model(self):构建模型# 创建问题self.prob pulp.LpProblem(Feed_Production_Optimization, pulp.LpMaximize)# 产品: 1普通饲料, 2高端饲料, 3预混料products [P1, P2, P3]# 决策变量: 产量(吨)self.variables {p: pulp.LpVariable(fx_{p}, lowBound0, catContinuous)for p in products}# 目标函数: 利润(元/吨)profit_coeffs {P1: 200, P2: 350, P3: 500}self.prob pulp.lpSum(profit_coeffs[p] * self.variables[p] for p in products), Total_Profit# 资源定义self.resources {R1: Resource(R1, 玉米, ResourceType.RAW_MATERIAL, 1200, 吨, Corn_Constraint),R2: Resource(R2, 豆粕, ResourceType.RAW_MATERIAL, 800, 吨, Soybean_Constraint),R3: Resource(R3, 预混料, ResourceType.RAW_MATERIAL, 200, 吨, Premix_Constraint),R4: Resource(R4, 制粒线, ResourceType.CAPACITY, 24, 小时, Pellet_Constraint),R5: Resource(R5, 包装线, ResourceType.CAPACITY, 20, 小时, Pack_Constraint),}# 原料消耗约束 (吨/吨产品)# P1: 玉米0.6, 豆粕0.3, 预混料0.1# P2: 玉米0.5, 豆粕0.4, 预混料0.1# P3: 玉米0.4, 豆粕0.2, 预混料0.4self.prob (0.6 * self.variables[P1] 0.5 * self.variables[P2] 0.4 * self.variables[P3] self.resources[R1].current_limit), self.resources[R1].constraint_nameself.prob (0.3 * self.variables[P1] 0.4 * self.variables[P2] 0.2 * self.variables[P3] self.resources[R2].current_limit), self.resources[R2].constraint_nameself.prob (0.1 * self.variables[P1] 0.1 * self.variables[P2] 0.4 * self.variables[P3] self.resources[R3].current_limit), self.resources[R3].constraint_name# 产能约束 (小时/吨产品)# P1: 制粒0.02h, 包装0.015h# P2: 制粒0.025h, 包装0.02h# P3: 制粒0.03h, 包装0.025hself.prob (0.02 * self.variables[P1] 0.025 * self.variables[P2] 0.03 * self.variables[P3] self.resources[R4].current_limit), self.resources[R4].constraint_nameself.prob (0.015 * self.variables[P1] 0.02 * self.variables[P2] 0.025 * self.variables[P3] self.resources[R5].current_limit), self.resources[R5].constraint_namedef solve(self, solverpulp.PULP_CBC_CMD(msgFalse)) - Tuple[float, float]:求解模型start_time time.perf_counter()self.prob.solve(solver)solve_time time.perf_counter() - start_timeobjective_value pulp.value(self.prob.objective)status pulp.LpStatus[self.prob.status]return objective_value, solve_time, statusdef perturb_resource(self, resource_id: str, perturbation: float,pert_type: PerturbationType PerturbationType.ABSOLUTE):扰动资源上限resource self.resources[resource_id]if pert_type PerturbationType.ABSOLUTE:new_limit resource.current_limit perturbationelse: # RELATIVEnew_limit resource.current_limit * (1 perturbation)# 找到对应约束并修改for constraint in self.prob.constraints.values():if resource.constraint_name in constraint.name:# 修改约束右端项constraint.constant -new_limitbreakreturn new_limitdef get_variable_values(self) - Dict[str, float]:获取决策变量值return {name: var.varValue for name, var in self.variables.items()}# ─── 影子价格分析器 ───────────────────────────────────────────────────────class ShadowPriceAnalyzer:影子价格批量分析器def __init__(self, model: FeedProductionModel):self.model modelself.base_objective Noneself.results: List[ScenarioResult] []def run_base_case(self) - float:运行基准场景print( 运行基准场景...)obj_value, solve_time, status self.model.solve()self.base_objective obj_valueresult ScenarioResult(scenario_idbase,resource_idall,original_limit0,perturbed_limit0,perturbation0,objective_valueobj_value,solve_timesolve_time,statusstatus)self.results.append(result)print(f 基准目标值: {obj_value:.2f} 元)print(f 求解耗时: {solve_time*1000:.1f} 毫秒)print(f 求解状态: {status})return obj_valuedef generate_perturbation_points(self,resource_id: str,pert_type: PerturbationType PerturbationType.ABSOLUTE,num_points: int 10,step: float 10.0) - List[PerturbationPoint]:生成扰动点resource self.model.resources[resource_id]if pert_type PerturbationType.ABSOLUTE:# 绝对扰动: -20%, -10%, 0, 10%, 20% 的资源量base resource.current_limitpoints [base * (1 i/100) - base for i in range(-20, 21, 5)]else:# 相对扰动: -0.2, -0.1, 0, 0.1, 0.2points [step * i for i in range(-num_points//2, num_points//2 1)]return [PerturbationPoint(resource_id, p, pert_type)for p in points]def analyze_resource(self,resource_id: str,pert_type: PerturbationType PerturbationType.ABSOLUTE,num_points: int 10,step: float 10.0) - ShadowPriceTable:分析单个资源的影子价格变化resource self.model.resources[resource_id]print(f\n 分析资源: {resource.name} (当前: {resource.current_limit}{resource.unit}))# 生成扰动点pert_points self.generate_perturbation_points(resource_id, pert_type, num_points, step)perturbations []objective_values []shadow_prices []solve_times []# 运行基准场景如果还没运行if self.base_objective is None:self.run_base_case()# 对每个扰动点求解for i, point in enumerate(pert_points):# 创建模型副本避免累积扰动model_copy FeedProductionModel()model_copy.perturb_resource(point.resource_id,point.perturbation_value,point.perturbation_type)# 求解obj_value, solve_time, status model_copy.solve()# 计算影子价格if i 0:shadow_price 0.0 # 基准点else:delta_z obj_value - self.base_objectivedelta_b point.perturbation_valueshadow_price delta_z / delta_b if delta_b ! 0 else 0.0perturbations.append(point.perturbation_value)objective_values.append(obj_value)shadow_prices.append(shadow_price)solve_times.append(solve_time)if i 0:print(f 基准: {obj_value:.2f} 元)else:print(f 扰动 {point.display_value}: {obj_value:.2f} 元, f影子价格{shadow_price:.2f} 元/{resource.unit})print(f 平均求解时间: {np.mean(solve_times)*1000:.1f} 毫秒/场景)return ShadowPriceTable(resource_idresource_id,resource_nameresource.name,perturbation_pointsperturbations,objective_valuesobjective_values,shadow_pricesshadow_prices)def analyze_all_resources(self,pert_type: PerturbationType PerturbationType.ABSOLUTE,num_points: int 10,step: float 10.0) - Dict[str, ShadowPriceTable]:分析所有资源的影子价格变化results {}for resource_id in self.model.resources:table self.analyze_resource(resource_id, pert_type, num_points, step)results[resource_id] tablereturn resultsdef generate_summary_report(self, tables: Dict[str, ShadowPriceTable]) - pd.DataFrame:生成汇总报告summary_data []for resource_id, table in tables.items():resource self.model.resources[resource_id]# 计算基准影子价格第一个正扰动点base_shadow_price table.shadow_prices[1] if len(table.shadow_prices) 1 else 0summary_data.append({资源ID: resource_id,资源名称: resource.name,资源类型: resource.resource_type.value,当前上限: resource.current_limit,单位: resource.unit,基准影子价格: round(base_shadow_price, 2),影子价格区间: f[{min(table.shadow_prices):.1f}, {max(table.shadow_prices):.1f}]})df pd.DataFrame(summary_data)df df.sort_values(基准影子价格, ascendingFalse)return df# ─── 报告生成器 ───────────────────────────────────────────────────────────class AnalysisReport:分析报告生成器staticmethoddef print_summary(summary_df: pd.DataFrame):print(\n 影子价格汇总表:)print( ─ * 70)print(summary_df.to_string(indexFalse))print(\n 决策建议:)print( • 影子价格最高的资源是瓶颈应优先保障)print( • 影子价格为0的资源有富余可适当减少)print( • 影子价格随资源变化说明存在非线性)staticmethoddef save_results(tables: Dict[str, ShadowPriceTable], output_dir: str output):保存结果到文件Path(output_dir).mkdir(exist_okTrue)# 保存每个资源的影子价格表for resource_id, table in tables.items():df table.to_dataframe()df.to_csv(f{output_dir}/shadow_price_{resource_id}.csv, indexFalse)# 绘制图表table.plot(save_pathf{output_dir}/shadow_price_{resource_id}.png)print(f\n 结果已保存到 {output_dir}/ 目录)# ─── 演示 ──────────────────────────────────────────────────────────────def demo():print( * 70)print( 资源扰动 → 影子价格批量分析器)print( 参考: 北理工《运筹学》第2章线性规划的对偶理论、第5章灵敏度分析)print( * 70)print(\n 场景: 饲料厂日排产, 5个资源, 批量扰动分析)print( 痛点: 人工改模型50次, 耗时45分钟, 曾多采购原料损失2.7万)print( 方案: Python批量扰动→0.8秒→生成影子价格变化表\n)# ── 1. 创建生产模型 ──print( ️ 构建饲料厂日排产模型...)start_time time.perf_counter()model FeedProductionModel()build_time time.perf_counter() - start_timeprint(f 模型构建完成)print(f 构建耗时: {build_time*1000:.1f} 毫秒)# ── 2. 创建分析器 ──print(\n 创建影子价格分析器...)analyzer ShadowPriceAnalyzer(model)# ── 3. 运行基准场景 ──print(\n 运行基准场景...)base_obj analyzer.run_base_case()base_time time.perf_counter() - start_time - build_time# ── 4. 分析所有资源 ──print(\n 批量分析5个资源的影子价格变化...)tables analyzer.analyze_all_resources(pert_typePerturbationType.ABSOLUTE,num_points9, # -20%, -15%, ..., 15%, 20%step5.0)利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛

相关资讯