1 前言
本文以两道经典建模题为例, 进一步介绍 Gurobi 与 Python 的交互, 以及其在建模中的应用. 阅读本文前, 建议读者先配置好 Gurobi 环境, 并且对数学建模有一定的认识 (吹水, 不考虑绝对的严谨性)。
本文也可作为建模小白的“入门指南”, 全文都是按照我的思维过程进行书写, (笔者在建模中承担建模 + 编程 + 模型主框架书写任务, 会将自己的模型以朴素的语言告知队友) 小白可以放心食用, 感受完成一道建模题所需要进行的分析工作.
2 Problem 1 最佳组队安排问题 (出处不详)
2.1 问题描述
在一年一度的我国和美国大学生数学建模竞赛活动中, 任何一个参赛院校都会遇到如何选拔最优秀的队员和科学合理地组队问题, 这是一个最实际的、而且首先需要解决的数学模型问题.
现假设有 20 名队员准备参加竞争, 根据队员的能力和水平要选出 18 名优秀队员分别组成 6 个队, 每个队 3 名队员去参加比赛. 选择队员主要考虑的条件依次为有关学科成绩(平均成绩)、智力水平(反映思维能力、分析问题和解决问题的能力等)、动手能力(计算机的使用和其他方面实际操作能力)、写作能力、外语能力、协作能力(团结协作能力)和其他特长. 每个队员的基本条件量化后如下表所示:
假设所有队员接受了同样的培训, 外部环境相同, 竞赛中不考虑其他的随机因素, 竞赛水平的发挥只取决于表中所给的各项条件, 并且参赛队员都能正常发挥自己的水平. 现在的问题是:
(1) 如何在20名队员中选拔18名优秀队员参加竞赛;
(2) 给出由18名队员组成6个队的组队方案, 使整体竞赛技术水平最高,并给出每个队的竞赛技术水平;
(3) 如果有更多的学生报名参加, 你的模型是否仍然可行?
2.2 问题分析
建模组队问题是经典的团队分工与总效益评估问题. 一般来说, 数学建模竞赛中的队员应该至少包含三项技能: 擅长文字写作与论文排版、灵活运用的算法思想与编程能力、优秀的数学基础与建模能力, 一般的分配就是三位同学各有专长. 这样的分配对于校方而言更有利于针对不同工作的学生进行集中培训, 避免资源浪费; 对于建模团队而言, 每位同学发挥自己的长处, 合作完成一篇建模论文也是大有裨益的.
笔者实践经验认为, 优秀的建模团队应该是每位队员都能独立地完成一道建模题目, 具备独立解决问题的基本技能, 在建模工作中相互渗透, 队员各有所长, 这有助于团队的协调分工、减少沟通成本.
但这样的队员可遇不可求, 退而求其次, 能做好自己的本职工作、具有较好的团队合作能力即可.
搜索资料容易查到武汉理工大学2019年美国大学生数学建模竞赛选拔, 可以作为参考. 建模队员的选拔, 往往根据不同的职位, 有不同的能力考察. 比如说编程的同学可能需要考虑算法功底、编程软件使用、动手实践能力、项目经验、智力水平等, 这也启发我们, 对于学生能力的评估, 不能用统一的评价体系一锅端. 我们要解决的第一个问题就是怎么选拔出适合从事单项工作的同学. 选拔出从事单项工作的队员后, 下一步要解决的问题就是如何进行组队. 计算方差, 让总体实力均衡? 计算平均数, 让总体能力均值最高? 似乎都可行, 但是联想到高中的时候开设的火箭班 (尖子班), 校方的考量应该是让优秀的同学冲刺更高的奖项 (更好的大学), 中间的学生尽可能拿奖 (不那么差的学校). 因此就涉及到第三个问题, 如何衡量一只队伍获奖的潜力.
基于以上分析, 解决该问题的思路为:
评价各个学生适合分别从事三项工作的得分值 -> 将学生进行工作分队 -> 建立获奖潜力评估模型 -> 组队安排, 让获奖的总效益最大化
当然, 注意到题目说的 “任何一个参赛院校都会遇到如何选拔最优秀的队员和科学合理地组队问题”, 以及第三问要求分析更多学生时的编排策略, 在建模中不应过早地将模型局限在 20 人选 6 只队伍上. 而应该建立一个普适性较强的模型. 再以20位同学的数据进行应用, 分析结论.
2.3 基本假设
(1) 学校通过对学生三种不同的能力评估 (定义为建模能力, 编程能力, 写作能力), 确定学生适合进行什么工作, 再进入各项工作的编制中;
(2) 学校根据高级别奖项优先原则, 在各个能力编制选出合适的队员组成一对;
(3) 假设所有学生在能力评估/竞赛中都能正常发挥;
(4) 假设每位同学都对参加选拔的同学的能力有一定的了解, 为了确保自己能被选上, 会选择对自己最有利的方向报名.
2.4 模型建立
2.4.1 学生单项能力综合评估
此部分较为简单, 能联想到需要根据不同的能力分别选取不同的指标, 建立三个评价模型即可. 当然, 作为建模团队而言, 建模同学能对写作、编程有一定的了解的话可能也是不错的选择, 即除了单独衡量一个方向外, 还可以结合上其他的能力值进行综合赋值.
我们假设三种方向主要考察以下指标:
建模能力评分: 科学成绩
编程能力评分: 智力水平
写作能力评分: 写作能力
TOPSIS法(优劣解距离法) 和 RSR(秩和比综合评价法) 都是简单方便的方法. 在评价建模能力的时候, 选择科学成绩、智力水平、其他特长作为评价指标, 使用 熵权法 + TOPSIS 方法进行评估, 设学生
考虑到建模中团队协作能力的影响, 定义学生
也可以考虑某位同学选择建模方向, 则主要考虑建模能力, 编程能力和写作能力值折扣记分, (或其他的换算函数), 如:
当然, 也可以对 “其他特长” 进一步细化考虑. 毕竟... 学习成绩同样的人, 特长越多越好么~
此部分总体思路如下:
2.4.2 团队总能力评估
定义团队总能力为
这里将团队各成员的单项能力得分作为总得分, 并人为规定顺序.
# 团队总能力评估
def team_score(model, code, write):
"""
团队能力 = 建模能力 * 协作能力 + 编程能力 * 协作能力 + 写作能力 * 协作能力
"""
return power_data.at[model, '建模'] + power_data.at[code, '编程'] + power_data.at[write, '写作']
2.4.3 获奖潜力评估
获奖概率难以量化, 但我们可以通过抽样仿真来进行估计.
假设该校所有报名的学生的能力得分分布与全体参加竞赛同学的得分分布近似. 这样我们就能通过对所有学生进行随机组合, 得到全体参加竞赛团队的成绩分布. 结合模型假设: 所有团队在竞赛中都能正常发挥, 那就可以按照团队实力的分布来代替他们比赛时的得分分布.
接着根据获奖比例, 设定三个获奖阈值, 只要团队总能力达到阈值水平就认为有可能获该奖. 达到
实际论文写作的话, 这里需要插入一张流程图.
定义指标变量:
仿真部分具体实现请看代码: (这里对整体水平 + 20%, 实际上可以多设几组, 观察组队变化情况)
# 仿真: 重复实验, 假设该校水平与整体水平相差 20%
team_scores = []
for i in range(21000):
team = list(power_data.sample(3, replace=True, random_state=i).index)
team_scores.append(team_score(*team) * 1.2)
# 排序, 设 .43% 特等奖, 8.88% 一等奖, 37.97% 二等奖
team_scores.sort(reverse=True)
g0 = team_scores[int(21000 * 0.0043)]
g1 = team_scores[int(21000 * 0.0932)]
g2 = team_scores[int(21000 * 0.3829)]
2.4.4 队员组队分配模型建立
假设某次建模比赛选拔中需要从
每位学生最多只能从事1项工作, 这是 0-1 指派问题. 同时, 要满足问题 (1) 选拔出优秀的队员, 再满足问题 (2) 进行组队分配, 这是个多目标优化模型.
定义决策变量:
定义第一级目标函数为全体同学的总能力得分最大化, 实际意义也很好理解, 优秀的团队应该从优秀的同学中产生:
定义第二级目标函数为获奖效益最大化:
10 : 5 : 1 是我瞎掰的, 可以自行设定. 也可以拆分为第三级、第四级目标.
约束条件:
① 每个团队每项工组都要有人负责
② 每一个人至多在一个队伍里负责1项工作
③ 每项任务都必须有m个人
④ 每个团队都必须有3个人
⑤ 获奖情况指标变量约束
实际上,约束 ③ 已经包含了 约束 ④
约束 ⑤ 中, 优化目标是全体,,尽可能大, 在可能的情况下 每一个都会尽量取1, 但取1就必须要满足团队的总能力得分大于各个奖项的得分阈值. 因此, 这样的约束就能用来转化为获奖队伍数量.
2.5 代码及求解
20选18的问题太简单了, 类似的改一下代码就行了
使用的编程语言:python3.7.1 (Anaconda3)
使用的编辑器:Sublime Text 3
使用的模块:pandas、statsmodels、scipy
代码一: topsis.py
文件
import pandas as pd
import numpy as np
def topsis(data, weight=None):
"""TOPSIS评价法"""
# 归一化
data = data / np.sqrt((data ** 2).sum())
# 最优最劣方案
Z = pd.DataFrame([data.min(), data.max()], index=['负理想解', '正理想解'])
# 距离
weight = entropyWeight(data) if weight is None else np.array(weight)
Result = data.copy()
Result['正理想解'] = np.sqrt(
((data - Z.loc['正理想解']) ** 2 * weight).sum(axis=1))
Result['负理想解'] = np.sqrt(
((data - Z.loc['负理想解']) ** 2 * weight).sum(axis=1))
# 综合得分指数
Result['综合得分指数'] = Result['负理想解'] / (Result['负理想解'] + Result['正理想解'])
Result['排序'] = Result.rank(ascending=False)['综合得分指数']
return Result, Z, weight
def entropyWeight(data):
"""熵权法"""
data = np.array(data)
# 归一化
P = data / data.sum(axis=0)
# 计算熵值
E = np.nansum(-P * np.log(P) / np.log(len(data)), axis=0)
# 计算权系数
return (1 - E) / (1 - E).sum()
代码二: main.py
文件
# -*- coding: utf-8 -*-
# @Author: suranyi
# @Date: 2019-03-30 11:32:17
# @Last Modified by: suranyi
# @Last Modified time: 2019-03-30 12:54:51
import numpy as np
import pandas as pd
import gurobipy
import topsis
# %% 1.创建测试数据
# 创建 600 名同学
classmates = [f'同学 {i}' for i in range(1, 601)]
# 组队队伍数
m = 100
# 能力
power = ['建模', '编程', '写作']
# 随机生成他们的成绩
np.random.seed(0)
data = pd.DataFrame(7 + np.random.randn(600, 7), index=classmates, columns=['科学成绩', '智力水平', '动手能力', '写作能力', '外语水平', '协作能力', '其他特长'])
# %% 2.能力值评估
# 单项能力评估
""" 熵权法赋权 + TOPSIS
建模能力: (科学成绩、智力水平、其他特长) * 协作能力
编程能力: (智力水平、动手能力、其他特长) * 协作能力
写作能力: (写作能力、外语水平、其他特长) * 协作能力
(建模能力 + 编程能力 + 写作能力) * 总协作能力 属于二次规划问题, 对于规模较小的模型可以快速解, 对于大规模模型难以求解
"""
power_data = pd.DataFrame(index=classmates, columns=power)
power_data['建模'] = topsis.topsis(data.loc[:, ['科学成绩', '智力水平', '其他特长']])[0]['综合得分指数'] * data.loc[:, '协作能力']
power_data['编程'] = topsis.topsis(data.loc[:, ['智力水平', '动手能力', '其他特长']])[0]['综合得分指数'] * data.loc[:, '协作能力']
power_data['写作'] = topsis.topsis(data.loc[:, ['写作能力', '外语水平', '其他特长']])[0]['综合得分指数'] * data.loc[:, '协作能力']
# 团队总能力评估
def team_score(model, code, write):
"""
团队能力 = 建模能力 * 协作能力 + 编程能力 * 协作能力 + 写作能力 * 协作能力
"""
return power_data.at[model, '建模'] + power_data.at[code, '编程'] + power_data.at[write, '写作']
# 仿真: 重复实验, 假设该校水平与整体水平相差 20%
team_scores = []
for i in range(21000):
team = list(power_data.sample(3, replace=True, random_state=i).index)
team_scores.append(team_score(*team) * 1.2)
# 排序, 设 .43% 特等奖, 8.88% 一等奖, 37.97% 二等奖
team_scores.sort(reverse=True)
g0 = team_scores[int(21000 * 0.0043)]
g1 = team_scores[int(21000 * 0.0932)]
g2 = team_scores[int(21000 * 0.3829)]
# %% 3.Gurobipy求解
# 创建模型
MODEL = gurobipy.Model()
# 创建变量
price = ["Outstanding Winner/Finalist", "Meritorious Winner", "Honorable Mention", "Successful Participant"]
x = MODEL.addVars(classmates, power, m, vtype=gurobipy.GRB.BINARY, name="X")
p = MODEL.addVars(m, price, vtype=gurobipy.GRB.BINARY, name='Price')
# 更新变量环境
MODEL.update()
# 创建目标函数
f = [sum(x[i, j, k] * power_data.at[i, j] for i in classmates for j in power) for k in range(m)] # 每只队伍的总能力值
MODEL.setObjectiveN(- sum(f), priority=3, index=0) # Obj1: 团队总得分最大
MODEL.setObjectiveN(- 10 * sum(p.select('*', price[0])) - 5 * sum(p.select('*', price[1])) - sum(p.select('*', price[2])), priority=2, index=1) # Obj2: 特等奖/一等奖人数尽可能多, 加权
# 创建约束条件
MODEL.addConstrs(sum(x.select('*', j, k)) == 1 for j in power for k in range(m)) # 每个团队每项工作都要有人负责
MODEL.addConstrs(sum(x.select(i, '*', '*')) <= 1 for i in classmates) # 每一个人至多在一个队伍里负责1项工作
MODEL.addConstrs(sum(x.select('*', j, '*')) == m for j in power) # 每项任务都必须有m个人
MODEL.addConstrs(sum(x.select('*', '*', k)) == 3 for k in range(m)) # 每个团队都必须有3个人
MODEL.addConstrs(p[k, price[0]] * g0 <= f[k] for k in range(m))
MODEL.addConstrs(p[k, price[1]] * g1 <= f[k] for k in range(m))
MODEL.addConstrs(p[k, price[2]] * g2 <= f[k] for k in range(m))
# 自动排序结果, 但该条件太苛刻了, 无法产生最优解
# for k in range(m - 1):
# MODEL.add