运筹学框架下SKU定价与库存优化模型构建及可行性问题求助
作为运筹学新手,现遇到如下优化问题:
给定一组SKU的常规价格、价格弹性、周均需求及库存水平,规则为:若按常规价格销售N周后库存仍不低于初始值的50%,可将价格下调10%-15%;目标是在M周内最大化收入并最小化剩余库存。
尝试用Pyomo搭建模型后求解提示问题不可行,恳请排查并提供修正建议。
from pyomo.environ import * sku_data = { 'SKU1': {'current_price': 100, 'price_elasticity': -0.2, 'current_demand': 50, 'current_inventory': 1000}, 'SKU2': {'current_price': 150, 'price_elasticity': -0.3, 'current_demand': 40, 'current_inventory': 1200}, } # Pyomo model model = ConcreteModel() # Sets model.SKUs = Set(initialize=sku_data.keys()) model.Weeks = RangeSet(1, 12) # 8 weeks in total (4 regular, 4 promo) # Parameters model.current_price = Param(model.SKUs, initialize=lambda model, sku: sku_data[sku]['current_price']) model.price_elasticity = Param(model.SKUs, initialize=lambda model, sku: sku_data[sku]['price_elasticity']) model.current_demand = Param(model.SKUs, initialize=lambda model, sku: sku_data[sku]['current_demand']) model.current_inventory = Param(model.SKUs, initialize=lambda model, sku: sku_data[sku]['current_inventory']) # Variables model.regular_price = Var(model.SKUs, within=NonNegativeReals, bounds=(0, None), initialize=lambda model, sku: sku_data[sku]['current_price']) model.promo_price_discount = Var(model.SKUs, within=NonNegativeReals, bounds=(0, 0.5), initialize=0.2) # Assume max discount is 50% model.promo_price = Var(model.SKUs, within=NonNegativeReals, bounds=(0, None), initialize=lambda model, sku: model.regular_price[sku] * (1 - model.promo_price_discount[sku])) model.regular_duration = Var(model.SKUs, within=NonNegativeIntegers, bounds=(4, None), initialize=4) model.promo_duration = Var(model.SKUs, within=NonNegativeIntegers, bounds=(0, None), initialize=0) def calculate_demand(base_price, new_price, elasticity, base_demand): return base_demand*(1 + elasticity * (new_price - base_price)/base_price) def calculate_inventory(base_inventory, base_demand, elasticity, regular_duration, promo_duration, regular_price, promo_price): inventory = base_inventory for t in range(1, 12): # Assuming 8 weeks in total (adjust as needed) if t <= regular_duration: inventory -= calculate_demand(regular_price, regular_price, elasticity, base_demand)*regular_duration elif t <= regular_duration + promo_duration: inventory -= calculate_demand(regular_price, promo_price, elasticity, base_demand)*promo_duration return inventory # Objective 1 is maximizing revenue model.obj = Objective(expr=sum( (calculate_demand( model.current_price[sku], model.regular_price[sku], model.price_elasticity[sku], model.current_demand[sku] ) * model.regular_price[sku]* model.regular_duration[sku] + calculate_demand( model.current_price[sku], model.promo_price[sku], model.price_elasticity[sku], model.current_demand[sku] ) * model.promo_price[sku] * model.promo_duration[sku]) for sku in model.SKUs ), sense=maximize) def inventory_constraint_rule(model, sku): base_inventory = model.current_inventory[sku] base_demand = model.current_demand[sku] regular_duration = value(model.regular_duration[sku]) elasticity = model.price_elasticity[sku] promo_duration = value(model.promo_duration[sku]) regular_price = model.regular_price[sku] promo_price = model.promo_price[sku] return calculate_inventory(base_inventory, base_demand, elasticity, regular_duration, promo_duration, regular_price, promo_price) >= 0 model.inventory_con = Constraint(model.SKUs, rule=inventory_constraint_rule) def promo_switch_constraint_rule(model, sku): base_inventory = model.current_inventory[sku] base_demand = model.current_demand[sku] regular_duration = value(model.regular_duration[sku]) elasticity = model.price_elasticity[sku] promo_duration = value(model.promo_duration[sku]) regular_price = model.regular_price[sku] promo_price = model.promo_price[sku] inventory_after_regular = calculate_inventory(base_inventory, base_demand, elasticity, 4, 0, regular_price, promo_price) # Check if promo switch is needed and set regular_duration accordingly return inventory_after_regular >= 0.5 * base_inventory model.promo_switch_con = Constraint(model.SKUs, rule=promo_switch_constraint_rule) # Solve the optimization problem solver = SolverFactory('ipopt') # Use an appropriate solver (e.g., 'glpk' or 'cbc') solver.solve(model, tee=True) # Display results for sku in model.SKUs: print(f"SKU: {sku}") print(f"Optimal Regular Price: {value(model.regular_price[sku])}") print(f"Optimal Promo Price: {value(model.promo_price[sku])}") print(f"Optimal Promo Price Discount: {value(model.promo_price_discount[sku])}") print(f"Optimal Regular Duration: {value(model.regular_duration[sku])}") print() # Access the optimal objective value optimal_revenue = value(model.obj) print(f"Optimal Revenue: {optimal_revenue}")
1. 库存计算函数逻辑严重错误
calculate_inventory函数中,每周扣减的需求量被错误乘以了regular_duration或promo_duration,导致库存被过度扣减(比如常规销售4周,每周扣减50*4=200,4周总共扣减800,远超实际需求)。正确逻辑应为每周扣减对应价格下的单周需求量:
def calculate_inventory(base_inventory, base_demand, elasticity, regular_duration, promo_duration, regular_price, promo_price): inventory = base_inventory # 先扣减常规销售期的库存 regular_demand = calculate_demand(regular_price, regular_price, elasticity, base_demand) inventory -= regular_demand * regular_duration # 再扣减促销期的库存 promo_demand = calculate_demand(regular_price, promo_price, elasticity, base_demand) inventory -= promo_demand * promo_duration return inventory
同时,函数中循环11周(range(1,12))与注释的8周矛盾,直接按时长计算更简洁,避免循环错误。
2. 约束中使用value()导致无效约束
在inventory_constraint_rule和promo_switch_constraint_rule中,调用value(model.regular_duration[sku])会直接取变量的初始值而非优化过程中的变量值,导致约束无法正确关联变量,变成固定值约束,这是模型不可行的核心原因之一。必须直接使用变量构建约束表达式,不能用value():
def inventory_constraint_rule(model, sku): regular_demand = calculate_demand(model.current_price[sku], model.regular_price[sku], model.price_elasticity[sku], model.current_demand[sku]) promo_demand = calculate_demand(model.current_price[sku], model.promo_price[sku], model.price_elasticity[sku], model.current_demand[sku]) return model.current_inventory[sku] - regular_demand * model.regular_duration[sku] - promo_demand * model.promo_duration[sku] >= 0
3. 促销折扣范围未按需求约束
问题要求折扣为10%-15%,但当前promo_price_discount的 bounds 是(0,0.5),初始化值0.2也不符合要求。需修正变量定义:
model.promo_price_discount = Var(model.SKUs, within=NonNegativeReals, bounds=(0.1, 0.15), initialize=0.12)
同时,promo_price可以直接用表达式定义为衍生变量,避免冗余:
model.promo_price = Expression(model.SKUs, rule=lambda model, sku: model.regular_price[sku] * (1 - model.promo_price_discount[sku]))
4. 时间总长度约束缺失
问题要求在M周内完成销售,但当前模型未限制regular_duration + promo_duration <= M(比如M=12),需添加约束:
def total_duration_rule(model, sku): return model.regular_duration[sku] + model.promo_duration[sku] <= 12 model.total_duration_con = Constraint(model.SKUs, rule=total_duration_rule)
另外,regular_duration的bounds设置为(4, None)不合理,应上限为M:
model.regular_duration = Var(model.SKUs, within=NonNegativeIntegers, bounds=(0, 12), initialize=4)
5. 目标函数未包含最小化剩余库存
原问题目标是最大化收入+最小化剩余库存,但当前仅最大化收入,需构建多目标函数(可通过加权法合并):
# 定义权重,比如收入权重0.8,剩余库存权重0.2(可调整) revenue_weight = 0.8 inventory_weight = 0.2 def obj_rule(model): total_revenue = sum( calculate_demand(model.current_price[sku], model.regular_price[sku], model.price_elasticity[sku], model.current_demand[sku]) * model.regular_price[sku] * model.regular_duration[sku] + calculate_demand(model.current_price[sku], model.promo_price[sku], model.price_elasticity[sku], model.current_demand[sku]) * model.promo_price[sku] * model.promo_duration[sku] for sku in model.SKUs ) total_remaining_inventory = sum( model.current_inventory[sku] - calculate_demand(model.current_price[sku], model.regular_price[sku], model.price_elasticity[sku], model.current_demand[sku]) * model.regular_duration[sku] - calculate_demand(model.current_price[sku], model.promo_price[sku], model.price_elasticity[sku], model.current_demand[sku]) * model.promo_duration[sku] for sku in model.SKUs ) # 最大化收入,最小化剩余库存等价于最大化(收入 - 加权剩余库存) return revenue_weight * total_revenue - inventory_weight * total_remaining_inventory model.obj = Objective(rule=obj_rule, sense=maximize)
6. 促销触发逻辑约束错误
原问题规则是:若按常规价格销售N周后库存≥初始50%,则可启动促销,当前promo_switch_con的逻辑是强制要求销售4周后库存≥50%,这不符合"可选择促销"的规则,应改为:如果启动促销(即promo_duration > 0),则必须满足销售N周后库存≥初始50%。需引入二进制变量表示是否启动促销:
# 添加二进制变量:1表示启动促销,0表示不启动 model.promo_active = Var(model.SKUs, within=Binary, initialize=0) # 促销触发约束:如果启动促销,那么销售N周(比如N=4)后的库存≥初始50% def promo_trigger_rule(model, sku): N = 4 regular_demand = calculate_demand(model.current_price[sku], model.regular_price[sku], model.price_elasticity[sku], model.current_demand[sku]) inventory_after_N = model.current_inventory[sku] - regular_demand * N # 大M法:当promo_active=1时,inventory_after_N >= 0.5*base_inventory;promo_active=0时无约束 return inventory_after_N >= 0.5 * model.current_inventory[sku] - (1 - model.promo_active[sku]) * 1e6 model.promo_trigger_con = Constraint(model.SKUs, rule=promo_trigger_rule) # 促销时长与促销激活变量关联:如果promo_active=0,则promo_duration=0 def promo_duration_rule(model, sku): return model.promo_duration[sku] <= model.promo_active[sku] * 12 # 12为最大可能时长 model.promo_duration_con = Constraint(model.SKUs, rule=promo_duration_rule)
7. 求解器选择问题
模型包含整数变量(regular_duration、promo_duration、promo_active),IPOPT是连续优化求解器,无法处理整数变量,需切换到支持混合整数非线性规划的求解器,比如bonmin或couenne,或调整变量为连续值(若允许时长为小数)。
内容的提问来源于stack exchange,提问作者user3490622

