Python+Cplex学习笔记(三)——docplex官⽅⽰例之营养膳⾷选择
1 前⾔
⽹上能到的python调⽤cplex⽰例实在太少,全英⽂的官⽅⽂档⼜⼗分难啃,还是从例⼦学起⽐较好,转了⼀圈到github,发现IBM写了⼏个例⼦,但是⼜没有中⽂博客解释。借助⽹页和help,代码看半天终于懂了个⼤概,搬运⼀下,同时记录菜鸟笔记,抛砖引⽟,欢迎⼤家批评指正。
2 IBM官⽅说明
IBM在下述⽹页中提供了DOcplex说明⼿册、⽰例、云端求解等内容,可以⼤致看⼀下按需获取。
在examples⽂件夹中,IBM给出了cp和mp的求解例⼦,我们主要关注mp问题。mp/modeling/中有6个例⼦,代码点进⼀看都很长,就先从第⼀个diet问题开始学习吧。
3 diet问题
3.1 背景及建模
该问题以最⼩化费⽤为⽬标,选择满⾜⽇常营养需求的⼀组⾷物。
⽬标函数:min总费⽤
每种⾷品费⽤ = 数量 × 单价,n种⾷品累加可得总费⽤
约束:满⾜各营养物质的取值范围要求
每种营养物质取值 = sum(数量 × 某⾷品中每单位该营养物质含量),n种⾷品累加
如:Calories,取值范围[2000, 2250]; Cholesterol,取值范围[0, 300]
3.2 代码详解
(1)导⼊库
from collections import namedtuple
from del import Model
from vironment import get_environment
分别⽤于命名元组、导⼊docplex和配置环境
(2)导⼊数据并初始化
FOODS =[
("Roasted Chicken",0.84,0,10),
("Spaghetti W/ Sauce",0.78,0,10),
("Tomato,Red,Ripe,Raw",0.27,0,10),
("Apple,Raw,W/Skin",.24,0,10),
("Grapes",0.32,0,10),
("Chocolate Chip Cookies",0.03,0,10),
("Lowfat Milk",0.23,0,10),
("Raisin Brn",0.34,0,10),
("Hotdog",0.31,0,10)
]
NUTRIENTS =[
("Calories",2000,2500),
("Calcium",800,1600),
("Iron",10,30),
("Vit_A",5000,50000),
("Dietary_Fiber",25,100),
("Carbohydrates",0,300),
("Protein",50,100)
]
FOOD_NUTRIENTS =[
("Roasted Chicken",277.4,21.9,1.8,77.4,0,0,42.2),
("Spaghetti W/ Sauce",358.2,80.2,2.3,3055.2,11.6,58.3,8.2),
("Tomato,Red,Ripe,Raw",25.8,6.2,0.6,766.3,1.4,5.7,1),
("Apple,Raw,W/Skin",81.4,9.7,0.2,73.1,3.7,21,0.3),
("Grapes",15.1,3.4,0.1,24,0.2,4.1,0.2),
("Chocolate Chip Cookies",78.1,6.2,0.4,101.8,0,9.3,0.9),
("Lowfat Milk",121.2,296.7,0.1,500.2,0,11.7,8.1),
("Raisin Brn",115.1,12.9,16.8,1250.2,4,27.9,4),
("Hotdog",242.1,23.5,2.3,0,0,18,10.4)
]
FOODS⽤于存储⾷品名称、单价、数量范围信息;NUTRIENTS⽤于存储各营养物质名称和取值范围;FOOD_NUTRIENTS⽤于存储⾷品内各营养物质含量。
Food = namedtuple("Food",["name","unit_cost","qmin","qmax"])
Nutrient = namedtuple("Nutrient",["name","qmin","qmax"])
使⽤namedtuple命名元组,使元组中的数据不仅能通过索引,也能通过⾃命名访问,增强可读性。
(3)模型建⽴函数
def build_diet_model(name='diet',**kwargs):
ints = kwargs.pop('ints',False)
⾸先定义build_diet_model函数⽤于建⽴模型。**kwargs⽤于不知道要往函数中传⼊多少个关键词参数,或者想传⼊字典的值作为关键词参数时。ints⽤于判断决策变量类型(选择⾷品时的单位是整型还是连续型)。
①数据存储形式处理
foods =[Food(*f)for f in FOODS]
nutrients =[Nutrient(*row)for row in NUTRIENTS]
*可传⼊任意个数的参数。这两条语句可使命名后的元组与原来的数据对应,同时可以print查看foods
和nutrients的具体存储信息。
foods = [Food(name=‘Roasted Chicken’, unit_cost=0.84, qmin=0, qmax=10), Food(name=‘Spaghetti W/ Sauce’,
unit_cost=0.78, qmin=0, qmax=10), Food(name=‘Tomato,Red,Ripe,Raw’, unit_cost=0.27, qmin=0, qmax=10),
Food(name=‘Apple,Raw,W/Skin’, unit_cost=0.24, qmin=0, qmax=10), Food(name=‘Grapes’, unit_cost=0.32, qmin=0, qmax=10), Food(name=‘Chocolate Chip Cookies’, unit_cost=0.03, qmin=0, qmax=10), Food(name=‘Lowfat Milk’,
unit_cost=0.23, qmin=0, qmax=10), Food(name=‘Raisin Brn’, unit_cost=0.34, qmin=0, qmax=10),
Food(name=‘Hotdog’, unit_cost=0.31, qmin=0, qmax=10)]
nutrients = [Nutrient(name=‘Calories’, qmin=2000, qmax=2500), Nutrient(name=‘Calcium’, qmin=800, qmax=1600), Nutrient(name=‘Iron’, qmin=10, qmax=30), Nutrient(name=‘Vit_A’, qmin=5000, qmax=50000),
Nutrient(name=‘Dietary_Fiber’, qmin=25, qmax=100), Nutrient(name=‘Carbohydrates’, qmin=0, qmax=300),
Nutrient(name=‘Protein’, qmin=50, qmax=100)]
food_nutrients ={(fn[0], nutrients[n].name):
fn[1+ n]for fn in FOOD_NUTRIENTS for n in range(len(NUTRIENTS))}
fn[0]输出FOOD_NUTRIENTS第1列,即⾷品名称;nutrients[n].name输出营养物质名称;fn[1 + n],n in range(0,7)输出
FOOD_NUTRIENTS的第2~8列,即营养物质含量。
为了将原来的FOOD_NUTRIENTS字典变成food_nutrients中的多键值形式,即{(⾷品i,营养物质j):值}的形式,需要对每⼀种⾷品循环7次(7种营养物质)。这样我们就可以根据⾷品名称+营养物质名称,快速查到各⾷物对应的不同营养含量。
最后得到:food_nutrients = {(‘Roasted Chicken’, ‘Calories’): 277.4, (‘Roasted Chicken’, ‘Calcium’): 21.9,
(‘Roasted Chicken’, ‘Iron’): 1.8, (‘Roasted Chicken’, ‘Vit_A’): 77.4, (‘Roasted Chicken’, ‘Dietary_Fiber’): 0, (‘Roasted Chicken’, ‘Carbohydrates’): 0, (‘Roasted Chicken’, ‘Protein’): 42.2, (‘Spaghetti W/ Sauce’,
‘Calories’): 358.2, (‘Spaghetti W/ Sauce’, ‘Calcium’): 80.2, (‘Spaghetti W/ Sauce’, ‘Iron’): 2.3, (‘Spaghetti W/ Sauce’, ‘Vit_A’): 3055.2, (‘Spaghetti W/ Sauce’, ‘Dietary_Fiber’): 11.6, (‘Spaghetti W/ Sauce’,
‘Carbohydrates’): 58.3, (‘Spaghetti W/ Sauce’, ‘Protein’): 8.2, (‘Tomato,Red,Ripe,Raw’, ‘Calories’): 25.8,
(‘Tomato,Red,Ripe,Raw’, ‘Calcium’): 6.2, (‘Tomato,Red,Ripe,Raw’, ‘Iron’): 0.6, (‘Tomato,Red,Ripe,Raw’,
‘Vit_A’): 766.3, (‘Tomato,Red,Ripe,Raw’, ‘Dietary_Fiber’): 1.4, (‘Tomato,Red,Ripe,Raw’, ‘Carbohydrates’): 5.7, (‘Tomato,Red,Ripe,Raw’, ‘Protein’): 1, (‘Apple,Raw,W/Skin’, ‘Calories’): 81.4, (‘Apple,Raw,W/Skin’,
‘Calcium’): 9.7, (‘Apple,Raw,W/Skin’, ‘Iron’): 0.2, (‘Apple,Raw,W/Skin’, ‘Vit_A’): 73.1, (‘Apple,Raw,W/S
kin’,‘Dietary_Fiber’): 3.7, (‘Apple,Raw,W/Skin’, ‘Carbohydrates’): 21, (‘Apple,Raw,W/Skin’, ‘Protein’): 0.3,
(‘Grapes’, ‘Calories’): 15.1, (‘Grapes’, ‘Calcium’): 3.4, (‘Grapes’, ‘Iron’): 0.1, (‘Grapes’, ‘Vit_A’): 24,
(‘Grapes’, ‘Dietary_Fiber’): 0.2, (‘Grapes’, ‘Carbohydrates’): 4.1, (‘Grapes’, ‘Protein’): 0.2, (‘Chocolate Chip Cookies’, ‘Calories’): 78.1, (‘Chocolate Chip Cookies’, ‘Calcium’): 6.2, (‘Chocolate Chip Cookies’, ‘Iron’): 0.4, (‘Chocolate Chip Cookies’, ‘Vit_A’): 101.8, (‘Chocolate Chip Cookies’, ‘Dietary_Fiber’): 0, (‘Chocolate Chip Cookies’, ‘Carbohydrates’): 9.3, (‘Chocolate Chip Cookies’, ‘Protein’): 0.9, (‘Lowfat Milk’, ‘Calories’): 121.2, (‘Lowfat Milk’, ‘Calcium’): 296.7, (‘Lowfat Milk’, ‘Iron’): 0.1, (‘Lowfat Milk’, ‘Vit_A’): 500.2, (‘Lowfat Milk’,‘Dietary_Fiber’): 0, (‘Lowfat Milk’, ‘Carbohydrates’): 11.7, (‘Lowfat Milk’, ‘Protein’): 8.1, (‘Raisin Brn’,
‘Calories’): 115.1, (‘Raisin Brn’, ‘Calcium’): 12.9, (‘Raisin Brn’, ‘Iron’): 16.8, (‘Raisin Brn’, ‘Vit_A’):
1250.2, (‘Raisin Brn’, ‘Dietary_Fiber’): 4, (‘Raisin Brn’, ‘Carbohydrates’): 27.9, (‘Raisin Brn’, ‘Protein’): 4, (‘Hotdog’, ‘Calories’): 242.1, (‘Hotdog’, ‘Calcium’): 23.5, (‘Hotdog’, ‘Iron’): 2.3, (‘Hotdog’, ‘Vit_A’): 0, (‘Hotdog’, ‘Dietary_Fiber’): 0, (‘Hotdog’, ‘Carbohydrates’): 18, (‘Hotdog’, ‘Protein’): 10.4}
②创建模型
mdl = Model(name=name,**kwargs)
③设置决策变量(⾷物数量)
ftype = mdl.integer_vartype if ints inuous_vartype
qty = mdl.var_dict(foods, ftype, lb=lambda f: f.qmin, ub=lambda f: f.qmax, name=lambda f:"q_%s"% f.name)
ftype表⽰决策变量类型,当调⽤函数是注明ints=True是整型,False则为连续型。qty表⽰每种⾷品的选择数量,下限lb取foods中的
f.qmin(⾄少选多少);上限ub取f.qmax(最多选多少)。变量命名为“q_⾷物名”,如q_Roasted Chicken。
④添加约束条件(营养物质取值范围)
for n in nutrients:
amount = mdl.sum(qty[f]* food_nutrients[f.name, n.name]for f in foods)
mdl.add_range(n.qmin, amount, n.qmax)
mdl.add_kpi(amount, publish_name="Total %s"% n.name)
对于每种营养物质,amount为各营养物质总值,qty[f]为⾷物数量,food_nutrients[f.name, n.name]调⽤字典得到各⾷物的不同营养取值,相乘后再累加。再通过mdl.add_range将营养物质总值限制在之前所给范围,⽤mdl.add_kpi在结果中将各营养物质总含量作为⼀项关键指标输出"Total 营养物质名”,如Total Calories。
⑤表⽰⽬标函数
total_cost = mdl.sum(qty[f]* f.unit_cost for f in foods)
mdl.add_kpi(total_cost,'Total cost')
各⾷品数量×单价 = 选择每种⾷品的花费,累加后得到总花费,同时将总花费Total cost也作为⼀项kpi输出。
⑥继续添加kpi——所选⾷物种类数
def nb_products(mdl_, s_):
qvs = mdl_.find_matching_vars(pattern="q_")
return sum(1for qv in qvs if s_[qv]>=1e-5)
mdl.add_kpi(nb_products,'Nb foods')
建⽴了另⼀个函数nb_products⽤于计算选择的⾷物种类数。使⽤find_matching_vars查包含字符串“q_”的变量,当“q_”有>0的数值时⾷物种类数sum += 1。
⑦⽬标函数最⼩化并返回mdl
mdl.minimize(total_cost)
return mdl
(4)求解模型并输出结果
if __name__ =='__main__':
mdl = build_diet_model(ints=True, log_output=True, float_precision=6)
mdl.print_information()
s = mdl.solve()
if s:
qty_vars = mdl.find_matching_vars(pattern="q_")
for fv in qty_vars:
food_name = fv.name[2:]
print("Buy {0:<25} = {1:9.6g}".format(food_name, fv.solution_value))
# Save the CPLEX solution as "solution.json" program output
with get_environment().get_output_stream("solution.json")as fp:
port(fp,"json")
else:
print("* model has no solution")
调⽤build_diet_model函数,决策变量为整型,输出⽇志⽂件,浮点数显⽰精度6位数;print_information()打印模型相关信息;
mdl.solve() ⽤于求解模型;port_kpis()显⽰关键指标数据;最后将解决⽅案另存为“ solution.json”程序输出。
if语句内进⾏格式化输出处理。fv = q_Roasted Chicken,…,截取后⾯字段得到⾷物名,输出模型结果“Buy ⾷物名 = 数量”,
如“Buy Spaghetti W/ Sauce = 2”。
其中,qty_vars = [docplex.mp.Var(type=I,name=‘q_Roasted Chicken’,ub=10), docplex.mp.Var(type=I,name=‘q_Spaghetti W/ Sauce’,ub=10), docplex.mp.Var(type=I,name=‘q_Tomato,Red,Ripe,Raw’,ub=10),
docplex.mp.Var(type=I,name=‘q_Apple,Raw,W/Skin’,ub=10), docplex.mp.Var(type=I,name=‘q_Grapes’,ub=10), docplex.mp.Var(type=I,name=‘q_Chocolate Chip Cookies’,ub=10), docplex.mp.Var(type=I,name=‘q_Lowfat
Milk’,ub=10), docplex.mp.Var(type=I,name=‘q_Raisin Brn’,ub=10), docplex.mp.Var(type=I,name=‘q_Hotdog’,ub=10)] 3.3 结果分析
最后程序输出如下:
先显⽰变量个数、约束个数等模型信息
再显⽰求解具体过程
分割线下的就是最后的模型求解结果了
买2单位Spaghetti W/ Sauce,1单位Apple,Raw,W/Skin,10单位Chocolate Chip Cookies,2单位Lowfat Milk和1单位Hotdog可以在满⾜营养物质要求的基础上实现费⽤最⼩化(Total cost = 2.87)。
此外还可以看到其他kpi信息,即各营养物质的总含量和⾷物种类数。
3.4 完整代码
# --------------------------------------------------------------------------
# Source file provided under Apache License, Version 2.0, January 2004,
# /licenses/
# (c) Copyright IBM Corp. 2015, 2018
# --------------------------------------------------------------------------
# The goal of the diet problem is to select a set of foods that satisfies
# a set of daily nutritional requirements at minimal cost.
# Source of data: /content/diet-problem-solver
from collections import namedtuple
from del import Model
from vironment import get_environment
# ----------------------------------------------------------------------------
# Initialize the problem data
# ----------------------------------------------------------------------------
FOODS =[
("Roasted Chicken",0.84,0,10),
("Spaghetti W/ Sauce",0.78,0,10),
("Tomato,Red,Ripe,Raw",0.27,0,10),
("Apple,Raw,W/Skin",.24,0,10),
("Grapes",0.32,0,10),
("Chocolate Chip Cookies",0.03,0,10),
("Lowfat Milk",0.23,0,10),
python官方文档中文版("Raisin Brn",0.34,0,10),
("Hotdog",0.31,0,10)
]
NUTRIENTS =[
("Calories",2000,2500),
("Calcium",800,1600),
("Iron",10,30),
("Vit_A",5000,50000),
("Dietary_Fiber",25,100),
("Carbohydrates",0,300),
("Protein",50,100)
]
FOOD_NUTRIENTS =[
("Roasted Chicken",277.4,21.9,1.8,77.4,0,0,42.2),
("Spaghetti W/ Sauce",358.2,80.2,2.3,3055.2,11.6,58.3,8.2),
("Tomato,Red,Ripe,Raw",25.8,6.2,0.6,766.3,1.4,5.7,1),
("Apple,Raw,W/Skin",81.4,9.7,0.2,73.1,3.7,21,0.3),
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论