first commit
commit
89132986a5
|
|
@ -0,0 +1,6 @@
|
||||||
|
__pycache__/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
other/
|
||||||
|
*.egg-info/
|
||||||
|
*.pdf
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
import sys
|
||||||
|
from PySide6 import QtCore, QtWidgets, QtGui
|
||||||
|
from PySide6.QtGui import QFont
|
||||||
|
from utils import read_excel, save_to_excel
|
||||||
|
from solve import solve_program
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class MyWidget(QtWidgets.QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.main_layout = QtWidgets.QVBoxLayout()
|
||||||
|
self.setLayout(self.main_layout)
|
||||||
|
|
||||||
|
bold_font = QFont()
|
||||||
|
bold_font.setBold(True) # 设置字体加粗
|
||||||
|
|
||||||
|
thin_font = QFont()
|
||||||
|
thin_font.setBold(False) # 设置字体不加粗
|
||||||
|
|
||||||
|
# 选择文件部分
|
||||||
|
self._excel_dir = None
|
||||||
|
self.group_box_1 = QtWidgets.QGroupBox("Step 1. 选择问卷星导出的表格文件")
|
||||||
|
self.group_box_1.setFont(bold_font)
|
||||||
|
|
||||||
|
self.openfile_layout = QtWidgets.QVBoxLayout()
|
||||||
|
self.button_openfile = QtWidgets.QPushButton("选择文件")
|
||||||
|
self.button_openfile.setFont(thin_font)
|
||||||
|
self.button_openfile.clicked.connect(self.open_file)
|
||||||
|
self.label_openfile = QtWidgets.QLabel("未选择文件")
|
||||||
|
self.label_openfile.setFont(thin_font)
|
||||||
|
|
||||||
|
self.openfile_layout.addWidget(self.button_openfile)
|
||||||
|
self.openfile_layout.addWidget(self.label_openfile)
|
||||||
|
self.group_box_1.setLayout(self.openfile_layout)
|
||||||
|
self.main_layout.addWidget(self.group_box_1)
|
||||||
|
|
||||||
|
|
||||||
|
# 限制条件部分
|
||||||
|
self.group_box_2 = QtWidgets.QGroupBox("Step 2. 输入限制条件")
|
||||||
|
self.group_box_2.setFont(bold_font)
|
||||||
|
self.cond_layout_overall = QtWidgets.QVBoxLayout()
|
||||||
|
self.group_box_2.setLayout(self.cond_layout_overall)
|
||||||
|
|
||||||
|
# 限制条件1
|
||||||
|
self.cond_layout_1 = QtWidgets.QHBoxLayout()
|
||||||
|
# 文字1
|
||||||
|
self.label_cond_1_1 = QtWidgets.QLabel("每班次人数:", self)
|
||||||
|
self.label_cond_1_1.setFont(thin_font)
|
||||||
|
# 数字输入框1
|
||||||
|
self.line_edit_1_1 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_1_1.setFont(thin_font)
|
||||||
|
self.line_edit_1_1.setValidator(QtGui.QIntValidator()) # 设置只接受整数
|
||||||
|
self.line_edit_1_1.setPlaceholderText("无限制")
|
||||||
|
self.line_edit_1_1.setText("4")
|
||||||
|
# 文字2
|
||||||
|
self.label_cond_1_2 = QtWidgets.QLabel("到", self)
|
||||||
|
self.label_cond_1_2.setFont(thin_font)
|
||||||
|
# 数字输入框2
|
||||||
|
self.line_edit_1_2 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_1_2.setFont(thin_font)
|
||||||
|
self.line_edit_1_2.setValidator(QtGui.QIntValidator()) # 设置只接受整数
|
||||||
|
self.line_edit_1_2.setPlaceholderText("无限制")
|
||||||
|
self.line_edit_1_2.setText("8")
|
||||||
|
|
||||||
|
self.cond_layout_1.addWidget(self.label_cond_1_1)
|
||||||
|
self.cond_layout_1.addWidget(self.line_edit_1_1)
|
||||||
|
self.cond_layout_1.addWidget(self.label_cond_1_2)
|
||||||
|
self.cond_layout_1.addWidget(self.line_edit_1_2)
|
||||||
|
self.cond_layout_overall.addLayout(self.cond_layout_1)
|
||||||
|
|
||||||
|
# 限制条件2
|
||||||
|
self.cond_layout_2 = QtWidgets.QHBoxLayout()
|
||||||
|
# 文字1
|
||||||
|
self.label_cond_2_1 = QtWidgets.QLabel("每班次电脑或电器的老人数:", self)
|
||||||
|
self.label_cond_2_1.setFont(thin_font)
|
||||||
|
# 数字输入框1
|
||||||
|
self.line_edit_2_1 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_2_1.setFont(thin_font)
|
||||||
|
self.line_edit_2_1.setValidator(QtGui.QIntValidator()) # 设置只接受整数
|
||||||
|
self.line_edit_2_1.setPlaceholderText("无限制")
|
||||||
|
# 文字2
|
||||||
|
self.label_cond_2_2 = QtWidgets.QLabel("到", self)
|
||||||
|
self.label_cond_2_2.setFont(thin_font)
|
||||||
|
# 数字输入框2
|
||||||
|
self.line_edit_2_2 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_2_2.setFont(thin_font)
|
||||||
|
self.line_edit_2_2.setValidator(QtGui.QIntValidator()) # 设置只接受整数
|
||||||
|
self.line_edit_2_2.setPlaceholderText("无限制")
|
||||||
|
self.line_edit_2_2.setText("1")
|
||||||
|
|
||||||
|
self.cond_layout_2.addWidget(self.label_cond_2_1)
|
||||||
|
self.cond_layout_2.addWidget(self.line_edit_2_1)
|
||||||
|
self.cond_layout_2.addWidget(self.label_cond_2_2)
|
||||||
|
self.cond_layout_2.addWidget(self.line_edit_2_2)
|
||||||
|
self.cond_layout_overall.addLayout(self.cond_layout_2)
|
||||||
|
|
||||||
|
# 限制条件3
|
||||||
|
self.cond_layout_3 = QtWidgets.QHBoxLayout()
|
||||||
|
# 文字1
|
||||||
|
self.label_cond_3_1 = QtWidgets.QLabel("每班次老人数:", self)
|
||||||
|
self.label_cond_3_1.setFont(thin_font)
|
||||||
|
# 数字输入框1
|
||||||
|
self.line_edit_3_1 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_3_1.setFont(thin_font)
|
||||||
|
self.line_edit_3_1.setValidator(QtGui.QIntValidator())
|
||||||
|
self.line_edit_3_1.setPlaceholderText("无限制")
|
||||||
|
self.line_edit_3_1.setText("1")
|
||||||
|
# 文字2
|
||||||
|
self.label_cond_3_2 = QtWidgets.QLabel("到", self)
|
||||||
|
self.label_cond_3_2.setFont(thin_font)
|
||||||
|
# 数字输入框2
|
||||||
|
self.line_edit_3_2 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_3_2.setFont(thin_font)
|
||||||
|
self.line_edit_3_2.setValidator(QtGui.QIntValidator())
|
||||||
|
self.line_edit_3_2.setPlaceholderText("无限制")
|
||||||
|
|
||||||
|
self.cond_layout_3.addWidget(self.label_cond_3_1)
|
||||||
|
self.cond_layout_3.addWidget(self.line_edit_3_1)
|
||||||
|
self.cond_layout_3.addWidget(self.label_cond_3_2)
|
||||||
|
self.cond_layout_3.addWidget(self.line_edit_3_2)
|
||||||
|
self.cond_layout_overall.addLayout(self.cond_layout_3)
|
||||||
|
|
||||||
|
# 限制条件4
|
||||||
|
self.cond_layout_4 = QtWidgets.QHBoxLayout()
|
||||||
|
# 文字1
|
||||||
|
self.label_cond_4_1 = QtWidgets.QLabel("每班次小朋友数:", self)
|
||||||
|
self.label_cond_4_1.setFont(thin_font)
|
||||||
|
# 数字输入框1
|
||||||
|
self.line_edit_4_1 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_4_1.setFont(thin_font)
|
||||||
|
self.line_edit_4_1.setValidator(QtGui.QIntValidator())
|
||||||
|
self.line_edit_4_1.setPlaceholderText("无限制")
|
||||||
|
self.line_edit_4_1.setText("2")
|
||||||
|
# 文字2
|
||||||
|
self.label_cond_4_2 = QtWidgets.QLabel("到", self)
|
||||||
|
self.label_cond_4_2.setFont(thin_font)
|
||||||
|
# 数字输入框2
|
||||||
|
self.line_edit_4_2 = QtWidgets.QLineEdit(self)
|
||||||
|
self.line_edit_4_2.setFont(thin_font)
|
||||||
|
self.line_edit_4_2.setValidator(QtGui.QIntValidator())
|
||||||
|
self.line_edit_4_2.setPlaceholderText("无限制")
|
||||||
|
|
||||||
|
self.cond_layout_4.addWidget(self.label_cond_4_1)
|
||||||
|
self.cond_layout_4.addWidget(self.line_edit_4_1)
|
||||||
|
self.cond_layout_4.addWidget(self.label_cond_4_2)
|
||||||
|
self.cond_layout_4.addWidget(self.line_edit_4_2)
|
||||||
|
self.cond_layout_overall.addLayout(self.cond_layout_4)
|
||||||
|
|
||||||
|
|
||||||
|
self.main_layout.addWidget(self.group_box_2)
|
||||||
|
|
||||||
|
|
||||||
|
# 处理文件部分
|
||||||
|
self.group_box_3 = QtWidgets.QGroupBox("Step 3. 获取最优结果")
|
||||||
|
self.group_box_3.setFont(bold_font)
|
||||||
|
|
||||||
|
self.button_solve = QtWidgets.QPushButton("开始排班!")
|
||||||
|
self.button_solve.setFont(thin_font)
|
||||||
|
self.text_solve = QtWidgets.QTextEdit(self)
|
||||||
|
self.text_solve.setFont(thin_font)
|
||||||
|
self.text_solve.setReadOnly(True) # 设置为只读模式
|
||||||
|
|
||||||
|
# 创建 QScrollArea 并将 QTextEdit 添加进去
|
||||||
|
self.scroll_area = QtWidgets.QScrollArea(self)
|
||||||
|
self.scroll_area.setWidget(self.text_solve) # 将 QTextEdit 设置为 QScrollArea 的内容
|
||||||
|
self.scroll_area.setWidgetResizable(True) # 允许 QTextEdit 根据 QScrollArea 的大小自动调整
|
||||||
|
|
||||||
|
self.button_solve.clicked.connect(self.magic)
|
||||||
|
|
||||||
|
self.solve_layout = QtWidgets.QVBoxLayout()
|
||||||
|
self.solve_layout.addWidget(self.scroll_area)
|
||||||
|
self.solve_layout.addWidget(self.button_solve)
|
||||||
|
self.group_box_3.setLayout(self.solve_layout)
|
||||||
|
self.main_layout.addWidget(self.group_box_3)
|
||||||
|
|
||||||
|
def open_file(self):
|
||||||
|
# 弹出文件选择对话框
|
||||||
|
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "打开文件", "", "所有文件 (*.*);;文本文件 (*.txt)")
|
||||||
|
|
||||||
|
# 如果用户选择了文件,则更新标签显示文件路径
|
||||||
|
if file_path:
|
||||||
|
self.label_openfile.setText(f"选中文件: {file_path}")
|
||||||
|
self._excel_dir = file_path
|
||||||
|
|
||||||
|
@QtCore.Slot()
|
||||||
|
def magic(self):
|
||||||
|
self.text_solve.append("*******************")
|
||||||
|
|
||||||
|
# 读取文件
|
||||||
|
if self._excel_dir is None:
|
||||||
|
self.text_solve.append("请先选择文件!")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.text_solve.append("开始排班...")
|
||||||
|
self.text_solve.append("读取文件中...")
|
||||||
|
all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array = read_excel(self._excel_dir)
|
||||||
|
self.text_solve.append("读取文件成功!")
|
||||||
|
|
||||||
|
self.text_solve.append("信息统计:")
|
||||||
|
stu_num = len(index_to_name_dict)
|
||||||
|
self.text_solve.append(f"\t学生总人数:{stu_num}")
|
||||||
|
|
||||||
|
class_num = len(preference_mat[0])
|
||||||
|
self.text_solve.append(f"\t班次总数:{class_num}")
|
||||||
|
|
||||||
|
tech_sum = 0
|
||||||
|
for i in range(len(preference_mat)):
|
||||||
|
if not is_new_array[i] and is_tech_array[i]:
|
||||||
|
tech_sum += want_num_array[i]
|
||||||
|
self.text_solve.append(f"\t电脑部或电气部的所有老人的意愿班次之和:{tech_sum}")
|
||||||
|
|
||||||
|
all_sum = sum(want_num_array)
|
||||||
|
self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}")
|
||||||
|
|
||||||
|
# 读取限制条件
|
||||||
|
num_min = int(self.line_edit_1_1.text()) if self.line_edit_1_1.text() else None
|
||||||
|
num_max = int(self.line_edit_1_2.text()) if self.line_edit_1_2.text() else None
|
||||||
|
num_tech_min = int(self.line_edit_2_1.text()) if self.line_edit_2_1.text() else None
|
||||||
|
num_tech_max = int(self.line_edit_2_2.text()) if self.line_edit_2_2.text() else None
|
||||||
|
num_old_min = int(self.line_edit_3_1.text()) if self.line_edit_3_1.text() else None
|
||||||
|
num_old_max = int(self.line_edit_3_2.text()) if self.line_edit_3_2.text() else None
|
||||||
|
num_new_min = int(self.line_edit_4_1.text()) if self.line_edit_4_1.text() else None
|
||||||
|
num_new_max = int(self.line_edit_4_2.text()) if self.line_edit_4_2.text() else None
|
||||||
|
|
||||||
|
self.text_solve.append("计算最优解中...")
|
||||||
|
vars = solve_program(preference_mat=preference_mat, want_num_array=want_num_array, is_new_array=is_new_array, is_tech_array=is_tech_array,
|
||||||
|
num_min=num_min, num_max=num_max, num_tech_min=num_tech_min, num_tech_max=num_tech_max,
|
||||||
|
num_old_min=num_old_min, num_old_max=num_old_max, num_new_min=num_new_min, num_new_max=num_new_max)
|
||||||
|
if vars is None:
|
||||||
|
self.text_solve.append("在目前限制条件下无解!请尝试更改限制条件!")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.text_solve.append("计算最优解成功!")
|
||||||
|
|
||||||
|
# 保存结果到 excel
|
||||||
|
self.text_solve.append("保存结果中...")
|
||||||
|
time_str = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
save_dir = f"result_{time_str}.xlsx"
|
||||||
|
if vars is not None:
|
||||||
|
save_to_excel(vars, all_data, index_to_name_dict, preference_mat, save_dir)
|
||||||
|
self.text_solve.append(f"保存结果成功!保存路径:{save_dir}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QtWidgets.QApplication([])
|
||||||
|
|
||||||
|
widget = MyWidget()
|
||||||
|
widget.setWindowTitle("EVA 值班排班软件")
|
||||||
|
widget.resize(600, 600)
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
from ortools.sat.python import cp_model
|
||||||
|
from utils import read_excel, save_to_excel
|
||||||
|
|
||||||
|
def solve_program(preference_mat:list,
|
||||||
|
want_num_array:list,
|
||||||
|
is_new_array:list,
|
||||||
|
is_tech_array:list,
|
||||||
|
num_min=None,
|
||||||
|
num_max=None,
|
||||||
|
num_tech_min=None,
|
||||||
|
num_tech_max=None,
|
||||||
|
num_old_min=None,
|
||||||
|
num_old_max=None,
|
||||||
|
num_new_min=None,
|
||||||
|
num_new_max=None
|
||||||
|
):
|
||||||
|
|
||||||
|
is_old_array = [not is_new for is_new in is_new_array]
|
||||||
|
is_not_tech_array = [not is_tech for is_tech in is_tech_array]
|
||||||
|
|
||||||
|
# 这是一个整数规划问题,我们使用 CP-SAT 求解器来解决这个问题
|
||||||
|
model = cp_model.CpModel()
|
||||||
|
|
||||||
|
# 定义变量的规模,一共有 N*M 个变量需要求解器求解
|
||||||
|
N = len(preference_mat) # 学生人数
|
||||||
|
M = len(preference_mat[0]) # 班次数
|
||||||
|
|
||||||
|
variables = []
|
||||||
|
for i in range(N):
|
||||||
|
row_vars = []
|
||||||
|
for j in range(M):
|
||||||
|
var = model.NewBoolVar(f"choice_{i}_{j}")
|
||||||
|
row_vars.append(var)
|
||||||
|
variables.append(row_vars)
|
||||||
|
|
||||||
|
# 添加约束:满足每位同学的意愿班次
|
||||||
|
for i in range(N):
|
||||||
|
model.Add(sum(variables[i]) == want_num_array[i])
|
||||||
|
|
||||||
|
# 添加约束:每个班次至少有n位同学
|
||||||
|
if num_min is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j] for i in range(N)) >= num_min)
|
||||||
|
|
||||||
|
# 添加约束:每个班次至多有n位同学
|
||||||
|
if num_max is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j] for i in range(N)) <= num_max)
|
||||||
|
|
||||||
|
# 添加约束:每班次最少包含n个电脑或电器的老人
|
||||||
|
if num_tech_min is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) >= num_tech_min)
|
||||||
|
|
||||||
|
# 添加约束:每班次最多包含n个电脑或电器的老人
|
||||||
|
if num_tech_max is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j]*is_old_array[i]*is_tech_array[i] for i in range(N)) <= num_tech_max)
|
||||||
|
|
||||||
|
# 添加约束:每班次至少包含n个老人
|
||||||
|
if num_old_min is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) >= num_old_min)
|
||||||
|
|
||||||
|
# 添加约束:每班次至多包含n个老人
|
||||||
|
if num_old_max is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j]*is_old_array[i] for i in range(N)) <= num_old_max)
|
||||||
|
|
||||||
|
# 添加约束:每班次至少包含n个小朋友
|
||||||
|
if num_new_min is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) >= num_new_min)
|
||||||
|
|
||||||
|
# 添加约束:每班次至多包含n个小朋友
|
||||||
|
if num_new_max is not None:
|
||||||
|
for j in range(M):
|
||||||
|
model.Add(sum(variables[i][j]*is_new_array[i] for i in range(N)) <= num_new_max)
|
||||||
|
|
||||||
|
# 优化目标:最大化同学的满意度
|
||||||
|
target_variables = [variables[i][j]*preference_mat[i][j] for i in range(N) for j in range(M)]
|
||||||
|
model.Maximize(sum(target_variables)) # Maximize the sum of these variables.
|
||||||
|
|
||||||
|
# 求解优化问题
|
||||||
|
solver = cp_model.CpSolver()
|
||||||
|
status = solver.Solve(model)
|
||||||
|
|
||||||
|
# 输出结果
|
||||||
|
variables_return = []
|
||||||
|
if status == cp_model.OPTIMAL:
|
||||||
|
print("Optimal solution found:")
|
||||||
|
for i in range(N):
|
||||||
|
row_solution = [solver.Value(variables[i][j]) for j in range(M)]
|
||||||
|
variables_return.append(row_solution)
|
||||||
|
# print(" ".join(map(str, row_solution)))
|
||||||
|
|
||||||
|
# Print the optimized value of the objective function.
|
||||||
|
print(f"Optimized objective value: {solver.ObjectiveValue()}")
|
||||||
|
|
||||||
|
return variables_return
|
||||||
|
|
||||||
|
elif status == cp_model.FEASIBLE:
|
||||||
|
print("A potentially suboptimal solution was found.")
|
||||||
|
for i in range(N):
|
||||||
|
row_solution = [solver.Value(variables[i][j]) for j in range(M)]
|
||||||
|
variables_return.append(row_solution)
|
||||||
|
# print(" ".join(map(str, row_solution)))
|
||||||
|
|
||||||
|
# Print the optimized value of the objective function.
|
||||||
|
print(f"Suboptimal objective value: {solver.ObjectiveValue()}")
|
||||||
|
|
||||||
|
return variables_return
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("No solution found.")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
index_to_departments = { # 部门编号和部门名称的对应关系
|
||||||
|
1: "电脑部",
|
||||||
|
2: "电器部",
|
||||||
|
3: "人资部",
|
||||||
|
4: "财外部",
|
||||||
|
5: "文宣部"
|
||||||
|
}
|
||||||
|
|
||||||
|
index_to_type = { # 老人/小朋友和对应的编号
|
||||||
|
1: "老人",
|
||||||
|
2: "小朋友"
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_excel(file_path):
|
||||||
|
# 读取 Excel 文件
|
||||||
|
df = pd.read_excel(file_path)
|
||||||
|
|
||||||
|
# 待返回的所有信息。下令 N 为学生人数,M 为班次数
|
||||||
|
index_to_name_dict = {} # 长度为 N 的字典,包含数组下标和学生姓名的对应关系
|
||||||
|
preference_mat = [] # N x M 的二维数组,表示每位学生对每个班次的满意度
|
||||||
|
want_num_array = [] # 长度为 N 的数组,表示每位学生想要值班的次数
|
||||||
|
is_new_array = [] # 长度为 N 的数组,表示每位学生是否是小朋友
|
||||||
|
is_tech_array = [] # 长度为 N 的数组,表示每位学生是否是电脑部或电器部成员
|
||||||
|
|
||||||
|
'''
|
||||||
|
遍历每一行,对于每一行:
|
||||||
|
i=1 -> 时间戳
|
||||||
|
i=6 -> 姓名
|
||||||
|
i=7 -> 部门编号
|
||||||
|
i=8 -> 老人/小朋友
|
||||||
|
i=9~28 -> 共20个班次,每个班次的意愿度
|
||||||
|
i=29 -> 想要值班的次数
|
||||||
|
|
||||||
|
考虑到原始填表信息中可能有某位同学多次提交的记录,先过滤一下冗余信息
|
||||||
|
'''
|
||||||
|
all_data = {} # 储存过滤后的数据
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
# 读取学生姓名,比较时间戳
|
||||||
|
name = row[6]
|
||||||
|
format = "%Y/%m/%d %H:%M:%S"
|
||||||
|
time_this = datetime.strptime(row[1], format)
|
||||||
|
if (name not in all_data) or (time_this > datetime.strptime(all_data[name][1], format)):
|
||||||
|
info_list = df.iloc[index].tolist()
|
||||||
|
all_data[name] = info_list
|
||||||
|
|
||||||
|
# 遍历过滤后的数据,组建待返回的信息
|
||||||
|
for index, (name, info_list) in enumerate(all_data.items()):
|
||||||
|
# 学生姓名
|
||||||
|
index_to_name_dict[index] = name
|
||||||
|
|
||||||
|
# 意愿度
|
||||||
|
preference = info_list[9:29]
|
||||||
|
preference_mat.append(preference)
|
||||||
|
|
||||||
|
# 想要值班的次数
|
||||||
|
want_num = info_list[29]
|
||||||
|
want_num_array.append(want_num)
|
||||||
|
|
||||||
|
# 是否是小朋友
|
||||||
|
is_new_array.append(index_to_type[info_list[8]] == "小朋友")
|
||||||
|
|
||||||
|
# 是否是电脑部或电器部成员
|
||||||
|
is_tech_array.append(index_to_departments[info_list[7]] in ["电脑部", "电器部"])
|
||||||
|
|
||||||
|
return all_data, index_to_name_dict, preference_mat, want_num_array, is_new_array, is_tech_array
|
||||||
|
|
||||||
|
def save_to_excel(variables, all_data, index_to_name_dict, preference_mat, file_path):
|
||||||
|
|
||||||
|
# 用一个list储存每一班的值班人员,该list中每个元素是一个存有若干dict的list,每个dict表示某一班的某一值班人员信息
|
||||||
|
all_result = []
|
||||||
|
max_single_class_num = 0 # 用于记录最大的班次人数,以便后写入 excel
|
||||||
|
for j in range(len(variables[0])):
|
||||||
|
on_duty_list = []
|
||||||
|
single_class_num = 0
|
||||||
|
for i in range(len(variables)):
|
||||||
|
if variables[i][j] == 1:
|
||||||
|
single_stu_info = {}
|
||||||
|
single_stu_info["name"] = index_to_name_dict[i]
|
||||||
|
single_stu_info["department"] = all_data[index_to_name_dict[i]][7]
|
||||||
|
single_stu_info["type"] = all_data[index_to_name_dict[i]][8]
|
||||||
|
single_stu_info["adjust"] = preference_mat[i][j] <= 0
|
||||||
|
on_duty_list.append(single_stu_info)
|
||||||
|
single_class_num += 1
|
||||||
|
on_duty_list = sorted(on_duty_list, key=lambda x: x["department"]) # 按部门编号升序排序
|
||||||
|
all_result.append(on_duty_list)
|
||||||
|
max_single_class_num = max(max_single_class_num, single_class_num)
|
||||||
|
|
||||||
|
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
|
||||||
|
|
||||||
|
# 创建一个新的工作簿
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
# 写入表头
|
||||||
|
width = 25
|
||||||
|
ws['B1'] = "星期一"
|
||||||
|
ws.column_dimensions['B'].width = width
|
||||||
|
ws['C1'] = "星期二"
|
||||||
|
ws.column_dimensions['C'].width = width
|
||||||
|
ws['D1'] = "星期三"
|
||||||
|
ws.column_dimensions['D'].width = width
|
||||||
|
ws['E1'] = "星期四"
|
||||||
|
ws.column_dimensions['E'].width = width
|
||||||
|
ws['F1'] = "星期五"
|
||||||
|
ws.column_dimensions['F'].width = width
|
||||||
|
ws['G1'] = "星期六"
|
||||||
|
ws.column_dimensions['G'].width = width
|
||||||
|
ws['H1'] = "星期日"
|
||||||
|
ws.column_dimensions['H'].width = width
|
||||||
|
|
||||||
|
start_index = []
|
||||||
|
ws.merge_cells(f'A2:A{2+max_single_class_num}')
|
||||||
|
ws['A2'] = "第一班"
|
||||||
|
start_index.append(2)
|
||||||
|
ws.merge_cells(f'A{2+max_single_class_num+1}:A{2+2*max_single_class_num+1}')
|
||||||
|
ws[f'A{2+max_single_class_num+1}'] = "第二班"
|
||||||
|
start_index.append(2+max_single_class_num+1)
|
||||||
|
ws.merge_cells(f'A{2+2*max_single_class_num+2}:A{2+3*max_single_class_num+2}')
|
||||||
|
ws[f'A{2+2*max_single_class_num+2}'] = "第三班"
|
||||||
|
start_index.append(2+2*max_single_class_num+2)
|
||||||
|
|
||||||
|
for duty_index, duty in enumerate(all_result):
|
||||||
|
for stu_index, stu in enumerate(duty):
|
||||||
|
col = chr(ord('B')+duty_index//3)
|
||||||
|
row = start_index[duty_index%3]+stu_index
|
||||||
|
str_info = f"{stu['name']} {index_to_departments[stu['department']]} {index_to_type[stu['type']]}"
|
||||||
|
if stu['adjust']:
|
||||||
|
ws[col+str(row)].fill = PatternFill(fill_type='solid', start_color='FF0000', end_color='FF0000')
|
||||||
|
ws[col+str(row)] = str_info
|
||||||
|
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
wb.save(file_path)
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# EVA 值班排班工具使用手册 - v1.0.0
|
||||||
|
|
||||||
|
## 前言
|
||||||
|
这是浙江大学学生 E 志者协会“排班工具软件”的使用手册,将简要地介绍该软件的使用方法和注意事项。请注意,这是面向使用者而非开发者的手册,如果想了解该工具的开发流程和所使用的技术,请移步至 github 仓库中的说明手册(尚未上传)。
|
||||||
|
|
||||||
|
## 软件运行环境&基本情况
|
||||||
|
- 本软件只能在 windows 系统上运行。
|
||||||
|
- 本软件在部分版本的 win10 和 win11 系统上均测试过,能够正常运行。但是不排除可能出现缺少相关 dll 文件而无法运行的情况,如果出现请联系开发者解决。
|
||||||
|
- 本软件将排班问题建模为组合优化问题,以“尽可能减少被调剂的人次”为优化目标。在指定的限制条件下,若有解,则得到的一定是最优解。
|
||||||
|
|
||||||
|
## 软件使用方法
|
||||||
|
双击运行可执行文件,进行以下三个步骤。
|
||||||
|
|
||||||
|
1. 选择待输入的问卷星结果 excel 表格。对于选择的 excel 表格,有以下格式要求:
|
||||||
|
- 第 1 行,即表头的名称不会影响软件运行,但需要保证每一列的数据的格式和含义正确
|
||||||
|
- 第 1 列为序号,不会用到,但需要有
|
||||||
|
- 第 2 列为提交答卷时间,格式为 "year/month/day hour:min:sec"
|
||||||
|
- 第 3 列为填表所用时间,不会用到,但需要有
|
||||||
|
- 第 4 列为来源,不会用到,但需要有
|
||||||
|
- 第 5 列为来源详情,不会用到,但需要有
|
||||||
|
- 第 6 列为 IP,不会用到,但需要有
|
||||||
|
- 第 7 列为姓名,这是每一位同学的唯一标识
|
||||||
|
- 第 8 列为部门序号,用一个数字表示,映射关系如下:
|
||||||
|
```python
|
||||||
|
index_to_departments = { # 部门编号和部门名称的对应关系
|
||||||
|
1: "电脑部",
|
||||||
|
2: "电器部",
|
||||||
|
3: "人资部",
|
||||||
|
4: "财外部",
|
||||||
|
5: "文宣部"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 第 9 列标识老人还是小朋友,用一个数字表示,映射关系如下:
|
||||||
|
```python
|
||||||
|
index_to_type = { # 老人/小朋友和对应的编号
|
||||||
|
1: "老人",
|
||||||
|
2: "小朋友"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 第 10 ~ 29 列,表示对于每个班次的意愿(1 代表有时间,0 代表没时间)。因为从周一第一班到周日第二班一共有 20 班,所以总共有 20 列。
|
||||||
|
- 第 30 列,表示愿意排几班,用一个数字表示。
|
||||||
|
|
||||||
|
我提供了一个名为“问卷星结果_样例输入.xlsx”的文件,以作为正确输入格式的参考。
|
||||||
|
|
||||||
|
本软件可以处理同一名同学多次填写问卷的情况,只要保证多次填写中“姓名”保持一致即可,软件将会自动选择最后一次填写的结果作为最终意愿。由于“姓名”是每位同学的唯一标识符,如果出现了某位同学“姓名”填错的情况,需要手动删除该姓名的记录。
|
||||||
|
|
||||||
|
如果出现了两位同学撞名的情况,需要在填表时“姓名”后加上后缀以作区分,比如“王五1”和“王五2”,否则就会当作一个人来看待。
|
||||||
|
|
||||||
|
2. 输入想要的限制条件。
|
||||||
|
|
||||||
|
本软件是以“同学接受调剂”作为大前提,“限制条件必须满足”的条件下,以“尽可能减少被调剂的同学人次数”作为最终目的来求解的。所以设置的限制条件越紧,最后就有可能越多的同学被调剂。
|
||||||
|
|
||||||
|
本软件预设了一些限制的参数。当然,你可以自由修改。我推荐多尝试几个不同的限制,如果最后只有很少数的人次被调剂,再人工调整下最终的排班表。
|
||||||
|
|
||||||
|
3. 开始排班
|
||||||
|
|
||||||
|
点击“开始排班!”按钮以开始排班,输出结果会以 excel 表格的形式输出,输出的文件名称是“result_<当前时间戳>.xlsx”
|
||||||
|
|
||||||
|
在输出结果中,被调剂的人次都会被标红。
|
||||||
|
|
||||||
|
输出结果不会自动分配每一班的组长,需要最后人工分配下。
|
||||||
Binary file not shown.
Loading…
Reference in New Issue