软工实践》结对项目 - 部门纳新智能匹配
结对成员
郑浩晖 031502442
刘晨瑶 031502522
项目Github链接
https://github.com/TheSkyFucker/DepartmentSelector
项目预览:
数据的生成
-
部分截图:
模拟生成流程
考虑的因素
对于学生:
-
学生编号
为了保证真实感,根据福大学生编号规则拆分为五块: [专业][年级][学院][班级][学号],分别取一定区间内的随机数。 -
空闲时间段
按上课时间(两节课100分钟)划分时间块,按时间块随机取2~10个。
其中,- 工作日:19:00-20:40;20:50-22:30
- 休息日:8:20-10:00;10:20-12:00;14:00-15:40;15:50-17:30;19:00-20:40;20:50-22:30
-
学生兴趣标签
从标签池随机抽取。 -
部门意愿
倾向时间不冲突和标签更匹配的部门。
在随机抽取时,对于时间冲突的部门加上了一个负权。
对于部门:
-
部门编号
按“D001,D002,...”顺序编号。 -
学生上限
模拟校内真实情况,各部门中大约1/4为校级,3/4为院级,校级部门纳新人数一般相对院级较多。因此,编号前5个部门从[10,15]中随机,编号后15个部门从[7,12]中随机。 -
部门特色标签
从事先构造好的“标签池”中随机[2,10]个。 -
常规活动时间段
时间区段同学生一致,活动时长以及开始时间具有30分钟的随机波动。
匹配程序
数据建模
思路(分配原则)
假设每名学生都是理性填报。
我们以尽量迎合意愿且相对公平的原则为前提,采用类似高考志愿模式来设计匹配算法。
考虑到,部门意愿的优先级排序,是学生有根据多方综合考虑的(包括兴趣爱好、特长以及空闲时间等)。因此我们不首先去做标签或时间匹配,而是以学生的意愿列表为依据去投报部门。
并且,假设“学生空余时间”满足每周一致,没有单双周的问题,且“部门活动时间”是常规活动,而非临时活动。那么,只要某部门和某学生的空闲时间有冲突,也就代表着每次该部门在该时段举行活动,此同学都无法参加。为满足第一次作业中“如果一个学生常规部门活动时间请假超过6次,将面临被淘汰”的条件,我们直接将空闲时间冲突的学生筛去。
算法流程如下:
对于每名同学,有如下步骤:
(1)投到下一轮志愿的部门;
(2)为每个部门执行淘汰算法;
(3)若中选,从空闲时间中扣去花在该部门的时间,回到(1)
其中,淘汰算法步骤如下:
(1)筛选掉时间冲突的
(2)若剩余人数不超过部门人数限制,则全部中选,退出
(3)为每个学生根据“兴趣标签”计算一个排序权值 = 标签匹配数 * 中选价值。定义中选价值为:(已中选部门数量+1)的倒数
(4)匹配值从高到低排序,淘汰多余的人数
我们认为,对于学生选择部门这件事而言,是符合意愿、宁缺毋滥、而非类似选导那样非要匹配一个不可的。
因此有如下可能的“异常”状态:
1.即使与某部门的标签再匹配,但只要不在学生的意愿列表中就绝不会中选。
2.多数人投报极个别部门,然后几乎全部落选。
对于此类“异常”我们的解释:
对于1) 高达5个的志愿数量几乎是可以避免“还有想去的部门但不够填”的状况,因此,把一个学生强制塞到不想去的部门是不可取的方案。
对于2) 这种情况发生的可能性是满足约定的一部分,完全取决于输入的数据的“好坏”,想要避免就应该在学生填报的时候,说明一下不要一个篮子装全部的鸡蛋的道理。
架构设计图
编码规范
https://github.com/TheSkyFucker/DepartmentSelector/blob/master/Docs/Coding Standard.md
代码体现
- 命名符合规范
std::string m_id; //部门编号
std::vector<Student *> m_students; //部门正式成员
int m_memberLimit; //学生上限
std::vector<TimeSegment> m_schedules; //活动时间
std::vector<std::string> m_tags; //兴趣标签
std::vector<Student *> m_tempStudents; //部门候选成员
- 函数功能专一
void Student::DeleteFreeTimes(TimeSegment aSegment) throw()
{
std::vector<TimeSegment> result;
for (auto m_freeTime : m_freeTimes)
{
auto tmpSegs = m_freeTime.Cut(aSegment);
for (auto aSeg : tmpSegs)
{
result.push_back(aSeg);
}
}
m_freeTimes = result;
}
- 测试驱动代码实现
源码管理
- 核心代码:2200行,去除自动生成文档后约1200行。测试代码:450行。
- commits:32次。
结果评估
指标条件
考虑到的指标条件有如下几条:
1.学生中选率——中选/总人数
2.部门收获率——纳新不为0的部门数/总部门数
3.学生满意度——(5-i+1)/5 ,i为中选第几志愿部门。(如,中选第一志愿的满意度为5分,第二志愿4分...)
4.部门满意度——标签的匹配数 * 中选价值
针对于我们的算法,第4点将不用于考量指标的优良,因为这正是分配算法的依据。而1、2点,根据之前的分析,用两个值做评定指标并不满足我们“符合意愿、宁缺毋滥”的原则,且相当依赖于输入数据的“优良”,存在数据的投机性(如果给输入测试的所有学生尽量多的志愿部门、空闲时间、标签,即便是百分之八九十的中选率,对评判优良也并没有意义)。
因此,我们选择满意度累加和均值作为评判指标,最终结果相较前二者而言更加客观。
根据“一组示例数据”的输出得到的评估结论是,
1.学生中选率: 40.7%
2.部门收获率:83.3%
3.中选的学生满意度:2.52(平均在第三、四志愿被录取)
观察可以发现被一、二志愿录取的这类同学普遍都是空闲时间和兴趣标签较多(10个左右),意愿部门报的也比较多(3~5个),并且空闲时间和标签匹配都和靠前的部门吻合。
没有被录用的学生可以发现他们的报名表要不然就是填报的数量太少,要不然就是志愿和tag对不上号。和我们的结论也比较相符。
至于为什么把看起来没能得到太理想的数值结果,却拿去作为了“好”的示例的原因是,(1) 相对中肯客观,没有走投机路线;(2) 比较能反应落选问题所在。
从中也可以得出对学生填报部门的建议:
- 志愿必须保证参加部门活动时间充裕
- 填报的部门方向尽量选择符合自己兴趣标签填的内容
- 不要所有的志愿部门都是人数上限较低的
- 尽快填写尽早报名,防止满员落选
结对编码的感受
对友打代码手速爆炸快,打起来超有气势,因为作业正值国庆期间,隔着屏幕感觉仿佛是在看游戏主播打代码。全程可以说是肥肠的愉快了。(x
我们的分工是,我主要写文档缕思路以及处理JSON,队友主要实现主流程和整合。
在结对过程中,仅仅是观察就能学到很多东西。国庆第一次视频就是研读了一遍商讨又修改了的代码规范,之后规规矩矩的遵守规范,因为背后还有人盯着...(啊泥成员变量又没加m_,啊泥这个函数功能不单一)
对友很认真,对源码管理也很严格,先拉出了dev分支来写,每次有做增改都commit,完成了整个类或模块才合并master。 get到了很多之前没有见过的操作(看到对面一键生成函数注释十分惊奇,下线马上也给自己的VS安了AssistX...)
不过唯一觉得比较遗憾的是,为了保证数据看起来的真实性,我们在“生成数据”上花了非常非常多的时间,最后测试出来的结果中选率不高,对友一直很纠结,但我觉得那种依赖数据投机得到的极高中选率并没有意义,按这个方案得到的这个结果虽然看着有点凄惨,但是客观来说其实是比较合理的.... 不过我认为,如果作业是采用直接提供数据的方式会更好一点,因为数据本身理应学生提供、是固定的,而且也在后期方案的自我评判和调参上更有针对性一点。
(对友在他博客里面有句话说“质量感人,数量感人”。OTL)
闪光点&分享
1.设计方案瓶颈的时候回去再翻一遍需求,能很快会有思路。(在和对友前期讨论的时候一字一字的看了不下五遍作业博客)
2.即使有分歧也能愉快交流是时间紧也能愉快完成的前提,诀窍在于退让之后发自内心的称赞,以及“三明治说话法”。XD
3.一个人打代码时常打着打着陷入脑子浆糊,能及时被思路清晰的另一方及时点醒就效率很高。“旁观者清”。
4.结对过程中双方一定要时刻保持思路状态同步状态,遇到问题及时的交互解决比起一个人苦想会愉快很多(结对编程的迷之乐趣)XD