dormitory.management.commands.assign_dormitory 源代码

import copy
import random
from collections import defaultdict

import numpy as np
import pandas as pd
from django.core.management.base import BaseCommand
from tqdm import trange

'''
有关reference文件夹的说明:
reference文件夹用于存放宿舍分配时的参考信息。
results.xlsx是新生问卷填写结果;
info.xlsx是学院提供的含有新生姓名、学号、生源地、生源高中的表格;
dorm.xlsx是学院提供的含有空余宿舍列表的表格;
dorm_assigned.xlsx是保存宿舍分配结果的目标文件。
'''


[文档] class Freshman: def __init__(self, data): self.data = data def __repr__(self): return repr(self.data)
[文档] class Dormitory: def __init__(self, id: int, remain: int, is_noisy: bool): self.id = id self.remain = remain self.stu = [] # 盥洗室和楼道口比较吵闹,此属性为 True,其他寝室为 False self.noisy = is_noisy
[文档] def add(self, student: Freshman): self.stu.append(student)
[文档] def check_must(self): ''' 一个宿舍必须满足的条件: 宿舍里的同学至少来自3个不同的省份 来自同一省份的2人不能来自同一所高中 ''' origin = [s.data['origin'] for s in self.stu] if len(set(origin)) < len(self.stu) - 1: return False elif len(set(origin)) == len(self.stu) - 1: indices = {} for i, p in enumerate(origin): indices[p] = indices.get(p, []) + [i] dupl = [v for k, v in indices.items() if len(v) > 1][0] hs = [self.stu[i].data['high_school'] for i in dupl] if hs[0] == hs[1]: return False return True
[文档] def check_better(self): ''' 计算宿舍得分,应用于交换优化场景。 宿舍计分项包括: 存在来自同一省份的同学减分,并针对北京地区特别操作 专业是否平均分配:2文2理 > 4文/4理 > 文理1:3 性格分配是否合理:尽量一个寝室不要多于两个内向 是否愿意和留学生/交换生同宿舍 衡量能接受的最低空调温度接近程度,计算方差,特别计算能否接受整夜开空调的统一程度 衡量起床时间、睡眠时间的接近程度,计算方差 睡眠困扰同学尽量远离盥洗室和楼梯口(用 Dormitory.noisy 衡量) 宿舍环境(尽量保证一个宿舍整洁条理的有2人/随性就好的有2人) 对室友期待(一个寝室尽量不要全部专注学习/全面发展) 在手动分配前,尽量保证宿舍是4人或3人的 ''' # Just return 0 for empty dormitories. Otherwise, using np.var raises a warning if len(self.stu) == 0: return 0 score = 0 origin = [s.data['origin'] for s in self.stu] if len(set(origin)) == len(self.stu) - 1: score -= 300 beijing = [s for s in origin if s == "北京"] if len(beijing) >= 2: score -= 700 major_score = sum([s.data['major'] for s in self.stu]) if major_score == 2: score += 1200 elif major_score == 0 or major_score == 4: score += 800 if len([s for s in self.stu if s.data['personality'] == 0]) > 2: score -= 600 # score += 8 * np.prod([s.data['international'] for s in self.stu]) ac_score = 20 * np.var([s.data['ac_temp'] for s in self.stu], ddof = 0) ac_score += (len(set([s.data['all_night_ac'] for s in self.stu])) - 1) * 400 score -= ac_score wake_score = np.var([s.data['wake'] for s in self.stu], ddof = 0) score -= 30 * wake_score sleep_score = np.var([s.data['sleep'] for s in self.stu], ddof = 0) score -= 30 * sleep_score if self.noisy and any(s.data['sleep_quality'] == 0 for s in self.stu): score -= 300 env_score = sum(s.data['environment'] for s in self.stu) if env_score == 2: score += 200 if env_score in (0, 4): score += 100 if len(set(s.data['expectation'] for s in self.stu)) == 1: score -= 200 stu_cnt_map = {4: 600, 3: 400, 2: 0, 1: 0, 0: 0, } score += stu_cnt_map.get(len(self.stu)) return score
[文档] def read_info() -> list[Freshman]: '''返回一个Freshman的list''' freshmen = [] df = pd.read_excel("/workspace/dormitory/references/results.xlsx") df2 = pd.read_excel("/workspace/dormitory/references/info.xlsx") for index, stu in df.iterrows(): data = defaultdict() data['name'] = stu["姓名"] data['gender'] = stu["性别"] data['sid'] = stu["学号"] data['origin'] = stu["生源地"] data['high_school'] = stu["生源高中"] data['major'] = stu["专业意向"] data['weight'] = stu["体重"] data['international'] = stu["是否愿意和留学生住一起"] data['wake'] = stu["你预期的大学生活起床时间"] data['sleep'] = stu["你预期的大学生活睡觉时间"] data['ac_temp'] = stu["夏天能接受的最低空调温度"] data['all_night_ac'] = stu["是否接受夏天整晚开空调"] data['personality'] = stu["你的性格"] data['sleep_quality'] = stu["你的睡眠质量是"] data['environment'] = stu["你希望你的宿舍环境是"] data['expectation'] = stu["你本人更希望大学生活是"] # 在info表格中,根据学号找到对应行,读取生源地和生源高中信息,保证信息准确 try: info_row = df2.loc[df2["学号"] == data['sid']].iloc[0] data['origin'] = info_row["省市"] except IndexError: import sys print('IndexError when consulting info.xlsx', data['name']) sys.exit(1) # 2024年的 info 表格不包含这个列,只能选择相信问卷里填的 # data['high_school'] = info_row["中学"] # 注意此处 map 的值要和 out_as_excel() 中对应 major_map = {"文科类": 0, "理工类": 1, } data['major'] = major_map.get(data['major']) data['weight'] = float(data['weight'].replace("kg", "")) international_map = {"愿意": 5, "都可以": 1, "不愿意": 0, } data['international'] = international_map.get(data['international']) wake_map = {"7点前": 0, "7~8点": 1, "8~9点": 2, "9-10点": 3, "10-11点": 4, "11点后": 5, } data['wake'] = wake_map.get(data['wake']) sleep_map = {"23点前": 0, "23-24点": 1, "24-1点": 2, "1-2点": 3, "2点后": 4, } data['sleep'] = sleep_map.get(data['sleep']) data['ac_temp'] = int(data['ac_temp'][:2]) ac_map = {"是": 1, "否": 0, } data['all_night_ac'] = ac_map.get(data['all_night_ac']) personality_map = {"内向型(独处时精力充沛;更封闭,更愿意在经挑选的小群体中分享个人的情况;不把兴奋说出来。)": 0, "适中型(介于二者之间,能够在内外向之间切换,在人群中乐意与人交谈结交朋友,同时也享受独处。)": 1, "外向型(与他人相处时精力充沛;易于“读”和了解,随意地分享个人情况;高度热情地社交。)": 2, } data['personality'] = personality_map.get(data['personality']) sleep_quality_map = {"浅眠型(易受声、光影响)": 0, "酣睡型(较少受影响,一觉到天亮)": 1, } data['sleep_quality'] = sleep_quality_map.get(data['sleep_quality']) environment_map = {"整洁条理": 0, "随性就好": 1, } data['environment'] = environment_map.get(data['environment']) expectation_map = {"专注学习": 0, "全面发展": 1} data['expectation'] = expectation_map.get(data['expectation']) freshman_data = dict(data) freshman = Freshman(freshman_data) freshmen.append(freshman) return freshmen
[文档] def read_dorm() -> tuple[list[Dormitory], list[Dormitory]]: '''返回两个Dormitory的list,分别代表男寝和女寝''' def read_from_sheet(sheet: str) -> list[Dormitory]: ''' Reads information from a specific sheet in the workbook. ''' dorm = [] df = pd.read_excel("/workspace/dormitory/references/dorm.xlsx", sheet_name = sheet) for index, room in df.iterrows(): rid = int(room["房间"]) if len(dorm) == 0 or dorm[-1].id != rid: if len(dorm) != 0: assert dorm[-1].id < rid, "Expect room number to be ascending order" # We can tell if a dormitory is noisy from its last two digits dorm.append(Dormitory(rid, 1, (rid % 100) in (12, 25, 35, 36, 38, 39, 40, 49, 64))) else: dorm[-1].remain += 1 # 注意,只选择了剩余床位为4的作为分配目标 return list(filter(lambda d: d.remain == 4, dorm)) return read_from_sheet("男生宿舍"), read_from_sheet("女生宿舍")
[文档] def assign_dorm() -> list[Dormitory]: ''' 分配宿舍算法: 执行若干次(250000次)随机交换(选取任一宿舍,选取任一床位), 衡量交换前后两宿舍得分之和,使得总得分最大化 ''' freshmen = read_info() male_dorm, female_dorm = read_dorm() # 初始随机分配 # TODO: 如果运气很烂,最后剩余4人无法分到同一宿舍,可能导致算法卡死。 for stu in freshmen: assigned = False while not assigned: male_vacant = [d for d in male_dorm if d.remain > 0] female_vacant = [d for d in female_dorm if d.remain > 0] if stu.data['gender'] == "男": dorm = random.choice(male_vacant) dorm.add(stu) if dorm.check_must(): dorm.remain -= 1 assigned = True else: dorm.stu.pop() else: dorm = random.choice(female_vacant) dorm.add(stu) if dorm.check_must(): dorm.remain -= 1 assigned = True else: dorm.stu.pop() # 随机交换 print('\033[36mProcessing male dormitories...\033[0m') epsilon = 0.3 for episode in trange(250000): rid1 = random.randint(0, len(male_dorm) - 1) rid2 = random.randint(0, len(male_dorm) - 1) if rid1 == rid2: continue room1: Dormitory = copy.deepcopy(male_dorm[rid1]) room2: Dormitory = copy.deepcopy(male_dorm[rid2]) if len(room1.stu) == 0 or len(room2.stu) == 0: continue o_score = room1.check_better() + room2.check_better() temp1: Dormitory = copy.deepcopy(room1) temp2: Dormitory = copy.deepcopy(room2) del male_dorm[max(rid1, rid2)] del male_dorm[min(rid1, rid2)] if random.random() < epsilon: if len(room1.stu) != 4 and len(room2.stu) != 4: temp2.add(temp1.stu.pop()) if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): room1 = temp1 room2 = temp2 o_score = room1.check_better() + room2.check_better() if len(room1.stu) == 0 or len(room2.stu) == 0: male_dorm.append(room1) male_dorm.append(room2) continue temp1: Dormitory = copy.deepcopy(room1) temp2: Dormitory = copy.deepcopy(room2) bid1 = random.randint(0, len(room1.stu) - 1) bid2 = random.randint(0, len(room2.stu) - 1) temp1.stu[bid1], temp2.stu[bid2] = temp2.stu[bid2], temp1.stu[bid1] if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): male_dorm.append(temp1) male_dorm.append(temp2) else: male_dorm.append(room1) male_dorm.append(room2) print("\033[35mProcessing female dormitories...\033[0m") for episode in trange(250000): rid1 = random.randint(0, len(female_dorm) - 1) rid2 = random.randint(0, len(female_dorm) - 1) if rid1 == rid2: continue room1: Dormitory = copy.deepcopy(female_dorm[rid1]) room2: Dormitory = copy.deepcopy(female_dorm[rid2]) if len(room1.stu) == 0 or len(room2.stu) == 0: continue o_score = room1.check_better() + room2.check_better() temp1: Dormitory = copy.deepcopy(room1) temp2: Dormitory = copy.deepcopy(room2) del female_dorm[max(rid1, rid2)] del female_dorm[min(rid1, rid2)] if random.random() < epsilon: if len(room1.stu) != 4 and len(room2.stu) != 4: temp2.add(temp1.stu.pop()) if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): room1 = temp1 room2 = temp2 o_score = room1.check_better() + room2.check_better() if len(room1.stu) == 0 or len(room2.stu) == 0: female_dorm.append(room1) female_dorm.append(room2) continue temp1: Dormitory = copy.deepcopy(room1) temp2: Dormitory = copy.deepcopy(room2) bid1 = random.randint(0, len(room1.stu) - 1) bid2 = random.randint(0, len(room2.stu) - 1) temp1.stu[bid1], temp2.stu[bid2] = temp2.stu[bid2], temp1.stu[bid1] if temp1.check_must() and temp2.check_must() and (temp1.check_better() + temp2.check_better() > o_score): female_dorm.append(temp1) female_dorm.append(temp2) else: female_dorm.append(room1) female_dorm.append(room2) male_dorm.sort(key=lambda d: d.id) female_dorm.sort(key=lambda d: d.id) dorm_result = male_dorm + female_dorm return dorm_result
[文档] def out_as_excel(dorm_result: list[Dormitory]): '''将结果导出为excel文件,存储在reference/dorm_assigned.xlsx下''' df = pd.DataFrame() major_list = ["文科类", "理工类"] international_list = ["不愿意", "都可以", "愿意"] wake_list = ["7点前", "7~8点", "8~9点", "9-10点", "10-11点", "11点后"] sleep_list = ["23点前", "23-24点", "24-1点", "1-2点", "2点后"] ac_list = ["否", "是"] personality_list = ["内向型", "适中型", "外向型"] sleep_quality_list = ["浅眠型", "酣睡型"] environment_list = ["整洁条理", "随性就好"] expectation_list = ["专注学习", "全面发展"] for dorm in dorm_result: for stu in dorm.stu: data = { "宿舍号": dorm.id, "姓名": stu.data['name'], "性别": stu.data['gender'], "学号": stu.data['sid'], "生源地": stu.data['origin'], "生源高中": stu.data['high_school'], "意向专业": major_list[stu.data['major']], "体重": stu.data['weight'], "是否愿意与留学生住在同一间宿舍?": international_list[stu.data['international'] % 3], "起床时间": wake_list[stu.data['wake']], "入睡时间": sleep_list[stu.data['sleep']], "夏天能接受的最低空调温度": stu.data['ac_temp'], "是否接受夏天整夜开空调": ac_list[stu.data['all_night_ac']], "性格": personality_list[stu.data['personality']], "睡眠质量": sleep_quality_list[stu.data['sleep_quality']], "希望宿舍环境": environment_list[stu.data['environment']], "对大学生活期待": expectation_list[stu.data['expectation']], "得分": dorm.check_better(), } temp_df = pd.DataFrame(data, index=[0]) df = pd.concat([df, temp_df], ignore_index=True) df.to_excel( '/workspace/dormitory/references/dorm_assigned.xlsx', index=False)
[文档] class Command(BaseCommand): help = "Assign dormitory."
[文档] def handle(self, *args, **options): out_as_excel(assign_dorm())