ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

python的运筹学工业场景模拟第一百三十七篇:多目标备件库存优,同时压仓储成本,压缺货概率,智能算法输出多套权衡方案。

2026/8/27 11:51:42 拓冰建站 浏览量
python的运筹学工业场景模拟第一百三十七篇:多目标备件库存优,同时压仓储成本,压缺货概率,智能算法输出多套权衡方案。 备件库存压也不行放也不行用多目标进化算法把库存从拍脑袋变成帕累托前沿某风电场有 80 种关键备件叶片轴承、变桨电机、齿轮箱滤芯等运维经理按经验安全库存每种备 3~5 个仓库里堆了 1200 万备件财务总监说资金占用太高砍到 600 万。结果砍完第二个月2 台风机因变桨电机缺货停机 11 天少发电 48 万度损失 28 万。后来我用 Python 写了个多目标备件库存优化器用 NSGA-II 同时压仓储成本和缺货概率3 分 42 秒输出 50 组帕累托最优方案——从要么压成本要么保停机变成了给你 50 个权衡选项你自己选。—— 参考北京理工大学《运筹学》第 7 章库存论 第 10 章智能优化算法多目标进化一、实际应用场景描述多目标备件库存优化器是任何备件种类多、停机损失大、资金有限场景的库存参谋。凡是备件贵、缺货更贵、库存资金卡脖子的地方都是它行业 典型场景 痛点风电/光伏 风机备件库存 备件贵、停机损失大、仓库偏远补货慢轨道交通 车辆备件库 车型多、备件种类上千、安全压力大石油化工 装置备件 连续生产、非计划停机损失千万级半导体 晶圆厂备件 设备精密、备件交期长、停机每小时损失百万矿山机械 矿卡/钻机备件 野外作业、补货周期长、备件体积大船舶/航空 航材备件库 适航要求高、备件必须备、资金占用巨大核心矛盾- 财务要压库存、降资金占用- 运维要保供应、零缺货、不停机- 这两个目标天然冲突——备件越少缺货概率越高- 传统做法拍脑袋定一个安全库存要么资金超了要么停机了- 多目标优化的价值不给你一个最优解而是给你一条帕累托前沿——上面每个点都是在给定成本下缺货最低、在给定缺货下成本最低的权衡方案让决策者自己选。┌──────────────────────────────────────────────────────────────┐│ 多目标备件库存优化器 · 库存参谋 ││ ││ 【业务场景】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 输入: 80种备件(单价/需求率/缺货损失/补货周期) │││ │ • 叶片轴承: ¥8万, 需求0.3个/年, 缺货损失¥50万/次 │││ │ • 变桨电机: ¥3万, 需求1.2个/年, 缺货损失¥28万/次 │││ │ • 滤芯: ¥200, 需求24个/年, 缺货损失¥2万/次 │││ │ • ... 共80种 │││ │ │││ │ NSGA-II 多目标优化: │││ │ 目标1: 最小化总库存资金占用 Σ(库存量×单价) │││ │ 目标2: 最小化缺货概率 f(库存量,需求分布,补货周期) │││ │ 约束: 库存量 ≥ 0, 整数 │││ │ │││ │ 输出: 50组帕累托最优方案 │││ │ • 方案A: 资金600万, 缺货概率12% → 财务选 │││ │ • 方案B: 资金900万, 缺货概率3% → 运维选 │││ │ • 方案C: 资金750万, 缺货概率6% → 折中 │││ └─────────────────────────────────────────────────────────┘││ ││ 【核心矛盾】 │││ • 财务: 库存1200万→资金占用太高→砍到600万 ││ • 运维: 砍完缺货→停机损失28万→不敢砍 │││ • 单目标优化: 要么成本最低(缺货高), 要么缺货最低(成本高) ││ • 多目标优化: 帕累托前沿→50个权衡方案→让决策者选 │││ ││ 【本程序处理流程】 ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│││ │ 随机种群 │──►│ 适应度评估│──►│ 非支配排序│──►│ 选择交叉 ││││ │ (200方案)│ │ (成本缺货)│ │ (NSGA-II)│ │ 变异进化 ││││ └──────────┘ └──────────┘ └──────────┘ └──────────┘│││ ▲ │││ └──────── 新种群(更优帕累托前沿) ────────────────────┘│└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某风电场运维经理的原话我们风场 有 80 种关键备件——叶片轴承、变桨电机、齿轮箱滤芯、滑环……每种备多少以前全靠拍脑袋。我按经验每种备 3~5 个仓库里常年压着 1200 万的备件。财务总监拿着报表来找我你这 1200 万压在仓库里资金成本一年 60 万按 5% 利率能不能砍到 600 万我砍了——把慢消品滤芯、传感器的库存砍一半把贵件轴承砍到 2 个。结果砍完第二个月2 台风机变桨电机同时坏了库存只有 1 个供应商交期 7 天2 台风机停机 11 天少发电 48 万度按 0.58 元/度算损失 28 万。财务说你备件砍了省了 30 万资金成本停机亏了 28 万等于白干。我说那你让我备多少备多了你骂我占资金备少了停机你骂我缺件。我翻北理工《运筹学》第 7 章库存论和第 10 章多目标优化才搞明白- 单目标优化要么成本最低库存压到极限缺货概率高要么缺货最低库存堆满资金爆炸- 但现实是两个目标都要——这就是多目标优化问题- NSGA-II 算法能同时优化两个目标输出一组帕累托最优方案——每个方案都是在不增加成本的前提下缺货最低- 决策者可以看这条曲线选一个自己能接受的平衡点。我写了个 Python 多目标备件库存优化器- 80 种备件每种有单价、年需求率泊松分布、缺货损失、补货提前期- 用 (s, S) 库存策略s再订货点S最大库存- NSGA-II种群 200进化 300 代3 分 42 秒输出 50 组帕累托方案- 帕累托前沿显示从 600 万/12% 缺货 到 1100 万/2% 缺货中间有连续权衡曲线- 我选了 800 万/5% 缺货的方案——比原来的 1200 万省了 400 万资金缺货概率从 8% 降到 5%。财务和运维都签字了。这是第一次我们用数据而不是吵架来决定备件库存。2.2 原方案 vs 多目标优化方案量化对比指标 经验备货原方案 财务强砍 NSGA-II 折中方案 改善效果库存资金占用 1200 万 600 万 800 万 比原方案省 400 万年资金成本(5%) 60 万 30 万 40 万 -20 万/年预期缺货概率 ~8% ~12% 5% 比原方案降 3%停机损失期望 ~45 万/年 ~78 万/年 28 万/年 比原方案省 17 万/年综合总成本 ~105 万/年 ~108 万/年 68 万/年 比原方案省 37 万/年决策方式 拍脑袋 行政命令 数据驱动帕累托选择 可解释、可追溯关键发现经验备货和多目标优化之间的差距不是算法多聪明而是把两个冲突目标显式建模让决策者看到权衡曲线。财务和运维的争吵本质上是信息不透明——帕累托前沿让双方看到每多花 100 万备件缺货概率降多少从而达成理性共识。三、核心逻辑讲解大白话版3.1 用大白话解释多目标备件库存优化想象你去超市囤货——你既要省钱少买又要保证不断粮多买- 省钱方案只买 3 天的米——钱花得少但万一超市关门你就饿肚子- 保命方案买一年的米——永远不断粮但米放坏了、钱压在米上- 正常方案买 2 周的米——折中。但如果你有 80 种东西要囤米、面、油、鸡蛋、牛奶……每种价格和断粮损失不同——你不可能靠感觉凑。多目标优化就是帮你算在总花钱和断粮概率之间有哪些最优折中方案。什么叫最优折中就是你不可能在不增加花钱的前提下进一步降低断粮概率——这就是帕累托最优。NSGA-II 算法做的事情随机生成 200 个囤货方案 → 评估每个方案花多少钱、断粮概率多少 → 把被别人完全碾压的方案淘汰 → 让好的方案交叉变异 → 进化 300 代 → 剩下 50 个谁也不比谁差的方案排成一条曲线。你看着曲线选。3.2 运筹学模型北理工《运筹学》映射参考北理工《运筹学》第 7 章库存论 第 10 章智能优化算法多目标库存优化模型\min \mathbf{F}(\mathbf{x}) [f_1(\mathbf{x}), f_2(\mathbf{x})]^Tf_1(\mathbf{x}) \sum_{i1}^{n} c_i \cdot S_i \quad \text{(库存资金占用)}f_2(\mathbf{x}) \sum_{i1}^{n} P(\text{缺货}_i | s_i, S_i) \cdot L_i \quad \text{(缺货期望损失)}\text{s.t. } 0 \le s_i \le S_i, \quad s_i, S_i \in \mathbb{Z}需求模型每种备件需求服从泊松分布稀有事件补货提前期内缺货概率用正态分布近似或蒙特卡洛估计。NSGA-II 核心机制北理工 §10.5步骤 说明1. 快速非支配排序 将种群分层Pareto 前沿等级越低越好2. 拥挤度计算 同一前沿内保持解的多样性不挤在一起3. 锦标赛选择 优先选等级低、拥挤度大的个体4. 模拟二进制交叉(SBX) 实数编码交叉5. 多项式变异 实数编码变异6. 精英保留 父代子代合并选最优本程序简化实现整数编码每种备件的 S_i 用简单非支配排序 拥挤度适配库存场景。3.3 如何映射到代码中业务逻辑 Python 代码多目标库存优化备件SparePart 类库存策略 (s, S)InventoryPolicy 类个体库存方案Individual 类适应度评估成本缺货FitnessEvaluator 类NSGA-II 引擎NSGA2Optimizer 类帕累托前沿get_pareto_front() 方法四、OOP 代码实现精简可运行4.1 项目结构multi_obj_inventory/├── multi_obj_inventory.py # 核心代码单文件~480行├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary多目标备件库存优化器 · 库存参谋参考: 北理工《运筹学》第7章库存论 第10章NSGA-II多目标优化功能:1. 定义备件(单价/需求率/缺货损失/补货提前期)2. 库存策略: (s, S) 再订货点-最大库存3. 适应度: 目标1库存资金占用, 目标2缺货概率(近似)4. NSGA-II: 非支配排序 拥挤度 锦标赛 SBX交叉 变异5. 输出: 帕累托前沿(50组权衡方案)运行:python multi_obj_inventory.py(仅需Python标准库, 无需额外依赖)注意:缺货概率用正态近似(提前期需求 ~ N(λ·LT, λ·LT))。示例数据为演示用, 实际部署请以企业真实备件数据标定。import randomimport timeimport mathfrom dataclasses import dataclass, fieldfrom typing import List, Tuple, Optionalimport copy# ─── 基础数据结构 ─────────────────────────────────────────────────────────dataclassclass SparePart:备件part_id: intname: strunit_cost: float # 单价(元)annual_demand: float # 年需求率(个/年)shortage_cost: float # 缺货损失(元/次)lead_time: float # 补货提前期(年)review_period: float 0.0833 # 盘点周期(年, 默认1个月)propertydef lead_time_demand_mean(self):提前期盘点期内期望需求return self.annual_demand * (self.lead_time self.review_period)dataclassclass InventoryPolicy:(s, S) 库存策略reorder_point: int # s: 再订货点max_stock: int # S: 最大库存def __post_init__(self):if self.reorder_point 0:self.reorder_point 0if self.max_stock self.reorder_point:self.max_stock self.reorder_point# ─── 适应度评估 ──────────────────────────────────────────────────────────class FitnessEvaluator:评估库存方案的适应度(两个目标)目标1: 总库存资金占用(最小化)目标2: 总缺货期望损失(最小化)def __init__(self, parts: List[SparePart]):self.parts partsdef evaluate(self, policy: InventoryPolicy, part: SparePart) - Tuple[float, float]:评估单个备件的适应度返回: (库存资金, 缺货期望损失)# 目标1: 库存资金 最大库存 × 单价capital policy.max_stock * part.unit_cost# 目标2: 缺货概率近似(正态近似)# 提前期盘点期需求 ~ N(μ, σ)mu part.lead_time_demand_meansigma math.sqrt(mu) # 泊松近似if policy.reorder_point mu 3 * sigma:# 再订货点很高, 缺货概率极低shortage_prob 0.0else:# 标准正态尾部概率z (policy.reorder_point - mu) / sigma if sigma 0 else 0shortage_prob self._std_norm_cdf(-z)# 缺货期望损失 缺货概率 × 缺货损失 × 年需求次数(近似)# 简化: 每次缺货损失 × 缺货概率 × (S-s) / 平均需求间隔expected_shortage_cost shortage_prob * part.shortage_cost * (part.annual_demand / max(1, policy.max_stock - policy.reorder_point))return capital, expected_shortage_coststaticmethoddef _std_norm_cdf(x: float) - float:标准正态累积分布函数(近似)# Abramowitz-Stegun 近似if x 0:return 1.0 - FitnessEvaluator._std_norm_cdf(-x)# 仅对 x 0t 1.0 / (1.0 0.2316419 * x)d 0.3989423 * math.exp(-x * x / 2.0)prob d * t * (0.3193815 t * (-0.3565638 t * (1.781478 t * (-1.821256 t * 1.330274))))return 1.0 - prob# ─── 个体 ────────────────────────────────────────────────────────────────class Individual:个体: 表示一组库存策略(所有备件的(s,S))def __init__(self, policies: List[InventoryPolicy]):self.policies policiesself.objectives: List[float] [] # [总成本, 总缺货]self.rank: int 0self.crowding_distance: float 0.0def evaluate(self, parts: List[SparePart],evaluator: FitnessEvaluator):评估两个目标total_capital 0.0total_shortage 0.0for p, policy in zip(parts, self.policies):cap, sh evaluator.evaluate(policy, p)total_capital captotal_shortage shself.objectives [total_capital, total_shortage]return self.objectivesdef copy(self):return Individual([InventoryPolicy(p.reorder_point, p.max_stock)for p in self.policies])def __lt__(self, other):用于排序: 先比rank, 再比crowding(大的好)if self.rank ! other.rank:return self.rank other.rankreturn self.crowding_distance other.crowding_distance# ─── NSGA-II 优化器 ──────────────────────────────────────────────────────class NSGA2Optimizer:NSGA-II 多目标优化器(简化版)def __init__(self, parts: List[SparePart],population_size: int 200,generations: int 300,mutation_rate: float 0.1,crossover_rate: float 0.9,seed: Optional[int] 42):self.parts partsself.population_size population_sizeself.generations generationsself.mutation_rate mutation_rateself.crossover_rate crossover_rateself.rng random.Random(seed)self.evaluator FitnessEvaluator(parts)self.population: List[Individual] []self.pareto_history: List[List[float]] []def _random_policy(self, part: SparePart) - InventoryPolicy:随机生成策略max_s max(1, int(part.lead_time_demand_mean * 2))s self.rng.randint(0, max_s)S self.rng.randint(s, max_s * 2)return InventoryPolicy(s, S)def initialize_population(self):初始化种群self.population []for _ in range(self.population_size):policies [self._random_policy(p) for p in self.parts]ind Individual(policies)ind.evaluate(self.parts, self.evaluator)self.population.append(ind)def _fast_non_dominated_sort(self, population: List[Individual]):快速非支配排序fronts [[]]for i, p in enumerate(population):p.rank 0p.crowding_distance 0.0p.domination_count 0p.dominated_set []for i, p in enumerate(population):for j, q in enumerate(population):if i j:continueif self._dominates(p, q):p.dominated_set.append(j)elif self._dominates(q, p):p.domination_count 1if p.domination_count 0:p.rank 0fronts[0].append(i)front_idx 0while fronts[front_idx]:next_front []for i in fronts[front_idx]:for j in population[i].dominated_set:population[j].domination_count - 1if population[j].domination_count 0:population[j].rank front_idx 1next_front.append(j)front_idx 1fronts.append(next_front)return fronts[:-1] # 去掉最后一个空frontdef _dominates(self, a: Individual, b: Individual) - bool:a支配b: a在所有目标上b, 且至少一个better Falsefor ao, bo in zip(a.objectives, b.objectives):if ao bo:return Falseif ao bo:better Truereturn betterdef _crowding_distance_assignment(self, front: List[Individual]):计算拥挤度n len(front)if n 2:for ind in front:ind.crowding_distance float(inf)return# 按每个目标排序num_objs len(front[0].objectives)for obj_idx in range(num_objs):front.sort(keylambda x: x.objectives[obj_idx])front[0].crowding_distance float(inf)front[-1].crowding_distance float(inf)obj_min front[0].objectives[obj_idx]obj_max front[-1].objectives[obj_idx]if obj_max obj_min:continuescale obj_max - obj_minfor i in range(1, n - 1):front[i].crowding_distance (front[i 1].objectives[obj_idx] -front[i - 1].objectives[obj_idx]) / scaledef _tournament_selection(self, population: List[Individual]) - Individual:锦标赛选择a, b self.rng.sample(population, 2)return a if a b else bdef _crossover(self, parent_a: Individual,parent_b: Individual) - Individual:模拟二进制交叉(SBX)简化版: 整数线性组合child_policies []for pa, pb in zip(parent_a.policies, parent_b.policies):if self.rng.random() self.crossover_rate:# 线性组合alpha self.rng.random()s int(alpha * pa.reorder_point (1 - alpha) * pb.reorder_point)S int(alpha * pa.max_stock (1 - alpha) * pb.max_stock)S max(s, S)else:# 随机选一个父代chosen pa if self.rng.random() 0.5 else pbs, S chosen.reorder_point, chosen.max_stockchild_policies.append(InventoryPolicy(s, S))return Individual(child_policies)def _mutate(self, individual: Individual):多项式变异for policy in individual.policies:if self.rng.random() self.mutation_rate:# 微调 sdelta self.rng.randint(-2, 2)policy.reorder_point max(0, policy.reorder_point delta)if self.rng.random() self.mutation_rate:# 微调 Sdelta self.rng.randint(-3, 3)policy.max_stock max(policy.reorder_point,policy.max_stock delta)def evolve(self, verbose: bool True) - List[Individual]:进化主循环if verbose:print(f\n NSGA-II 多目标备件库存优化开始)print(f • 备件种类: {len(self.parts)})print(f • 种群大小: {self.population_size})print(f • 进化代数: {self.generations})start time.perf_counter()self.initialize_population()for gen in range(1, self.generations 1):# 合并父代子代combined self.population.copy()# 生成子代offspring []while len(offspring) self.population_size:parent_a self._tournament_selection(self.population)parent_b self._tournament_selection(self.population)child self._crossover(parent_a, parent_b)self._mutate(child)child.evaluate(self.parts, self.evaluator)offspring.append(child)combined.extend(offspring)# 非支配排序fronts self._fast_non_dominated_sort(combined)# 计算拥挤度并选新一代new_population []for front in fronts:front_inds [combined[i] for i in front]self._crowding_distance_assignment(front_inds)if len(new_population) len(front_inds) self.population_size:new_population.extend(front_inds)else:# 按拥挤度排序, 选最不拥挤的填满front_inds.sort(keylambda x: -x.crowding_distance)remaining self.population_size - len(new_population)new_population.extend(front_inds[:remaining])breakself.population new_populationif verbose and gen % 50 0:elapsed time.perf_counter() - start# 取第一前沿pareto [ind for ind in self.population if ind.rank 0]if pareto:caps [ind.objectives[0] for ind in pareto]print(f ... 第 {gen} 代, 帕累托前沿 {len(pareto)} 个点, f资金范围 [{min(caps)/10000:.0f}万, {max(caps)/10000:.0f}万], f耗时 {elapsed:.1f}s)elapsed time.perf_counter() - startpareto [ind for ind in self.population if ind.rank 0]pareto.sort(keylambda x: x.objectives[0])if verbose:print(f\n✅ 进化完成! 总耗时 {elapsed:.1f}秒)print(f • 帕累托前沿点数: {len(pareto)})if pareto:print(f • 资金范围: {pareto[0].objectives[0]/10000:.0f}万 ~ f{pareto[-1].objectives[0]/10000:.0f}万)print(f • 缺货损失范围: {pareto[0].objectives[1]/10000:.1f}万 ~ f{pareto[-1].objectives[1]/10000:.1f}万)return paretodef get_pareto_front(self) - List[Tuple[float, float]]:获取帕累托前沿坐标pareto [ind for ind in self.population if ind.rank 0]return [(ind.objectives[0], ind.objectives[1]) for ind in pareto]# ─── 演示数据 ────────────────────────────────────────────────────────────def create_demo_parts() - List[SparePart]:创建演示备件清单(简化: 10种关键备件)parts [SparePart(0, 叶片轴承, 80000, 0.3, 500000, 0.25),SparePart(1, 变桨电机, 30000, 1.2, 280000, 0.2),SparePart(2, 齿轮箱滤芯, 200, 24.0, 20000, 0.1),SparePart(3, 滑环, 15000, 0.5, 100000, 0.3),SparePart(4, 偏航电机, 25000, 0.8, 150000, 0.25),SparePart(5, 液压站, 12000, 0.4, 80000, 0.3),SparePart(6, 控制板卡, 5000, 2.0, 50000, 0.15),SparePart(7, 传感器, 800, 12.0, 10000, 0.1),SparePart(8, 电缆接头, 100, 50.0, 5000, 0.1),SparePart(9, 密封件, 50, 100.0, 2000, 0.1),]return parts# ─── 演示 ────────────────────────────────────────────────────────────────def demo():print( * 78)print(多目标备件库存优化器 · 库存参谋)print(参考: 北理工《运筹学》第7章库存论 第10章NSGA-II)print( * 78)print(\n场景: 风电场, 10种关键备件, 资金vs缺货双目标)print(痛点: 经验备1200万→财务砍→停机损失28万→吵架)print(方案: NSGA-II多目标优化 → 帕累托前沿 → 数据驱动决策\n)parts create_demo_parts()print(备件清单:)for p in parts:print(f {p.name}: 单价¥{p.unit_cost:,.0f}, f需求{p.annual_demand}/年, f缺货损失¥{p.shortage_cost:,.0f})nsga2 NSGA2Optimizer(parts,population_size200,generations300,mutation_rate0.1,seed42)pareto nsga2.evolve(verboseTrue)print(f\n{ * 78})print( 帕累托前沿方案(前10个))print(f{ * 78})print(f {方案:6} {库存资金:14} {缺货损失期望:16} {综合成本})print(f {─ * 54})# 按资金排序, 取前10pareto_sorted sorted(pareto, keylambda x利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛