使用梅校的算法
This commit is contained in:
@@ -3,80 +3,208 @@ from __future__ import annotations
|
||||
from io import BytesIO
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
|
||||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
|
||||
def _style_header(ws, row: int, ncols: int):
|
||||
fill = PatternFill("solid", fgColor="12233D")
|
||||
font = Font(color="F4EFE6", bold=True)
|
||||
for c in range(1, ncols + 1):
|
||||
cell = ws.cell(row, c)
|
||||
cell.fill = fill
|
||||
cell.font = font
|
||||
cell.alignment = Alignment(wrap_text=True, vertical="center")
|
||||
|
||||
|
||||
def _borders(ws, min_row: int = 1, max_row: int | None = None, max_col: int | None = None):
|
||||
thin = Side(style="thin", color="D9D1C3")
|
||||
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||
max_row = max_row or ws.max_row
|
||||
max_col = max_col or ws.max_column
|
||||
for row in ws.iter_rows(min_row=min_row, max_row=max_row, min_col=1, max_col=max_col):
|
||||
for cell in row:
|
||||
if cell.value is not None:
|
||||
cell.border = border
|
||||
|
||||
|
||||
def _group_order(g: str):
|
||||
return 0 if g == "物理" else 1 if g == "历史" else 2
|
||||
|
||||
|
||||
def _safe_title(text: str) -> str:
|
||||
return text.replace(":", "-").replace("/", "-").replace("\\", "-").replace("?", "-")[:31]
|
||||
|
||||
|
||||
def _raw_sheet(wb: Workbook, group: dict, students: list[dict]):
|
||||
title = _safe_title(f"{group['group']}方向人员")
|
||||
ws = wb.create_sheet(title)
|
||||
exams = group["_exams"]
|
||||
ws.append(["学号", "姓名", "班级", "方向", "选科", "学籍", "异动"] + exams)
|
||||
_style_header(ws, 1, ws.max_column)
|
||||
for s in students:
|
||||
ws.append(
|
||||
[
|
||||
s["sid"], s["name"], s["class_name"], s["track"], s.get("subjects") or "",
|
||||
s.get("status") or "", s.get("move") or "",
|
||||
]
|
||||
+ [("缺考" if s["scores"].get(e) is None else s["scores"].get(e)) for e in exams]
|
||||
)
|
||||
_borders(ws, 2)
|
||||
ws.freeze_panes = "A2"
|
||||
return ws
|
||||
|
||||
|
||||
def _probability_sheet(wb: Workbook, group: dict):
|
||||
title = _safe_title(f"{group['group']}类概率分析")
|
||||
ws = wb.create_sheet(title)
|
||||
exams = group["_exams"]
|
||||
lines = {x["id"]: x for x in group["targets"]}
|
||||
pair_blocks = {x["line"]: x["pairs"] for x in group["pairs"]}
|
||||
r = 1
|
||||
|
||||
def pair_for(line_id: str, a: str, b: str):
|
||||
line_name = lines[line_id]["name"]
|
||||
return next(
|
||||
(p for p in pair_blocks[line_name] if p["from_exam"] == a and p["to_exam"] == b),
|
||||
None,
|
||||
)
|
||||
|
||||
for idx, (a, b) in enumerate(zip(exams, exams[1:]), 1):
|
||||
top_pair = pair_for("top", a, b)
|
||||
control_pair = pair_for("control", a, b)
|
||||
if not top_pair or not control_pair:
|
||||
continue
|
||||
ws.cell(r, 1, f"情况{idx}·尖子线前{lines['top']['k']}名")
|
||||
ws.cell(r, 8, f"情况{idx}·特控线前{lines['control']['k']}名")
|
||||
r += 1
|
||||
for c0, line_id in ((1, "top"), (8, "control")):
|
||||
ws.cell(r, c0, "分层")
|
||||
ws.cell(r, c0 + 1, "排名区间")
|
||||
ws.cell(r, c0 + 2, a)
|
||||
ws.cell(r, c0 + 3, b)
|
||||
ws.cell(r, c0 + 4, "概率")
|
||||
_style_header(ws, r, 12)
|
||||
r += 1
|
||||
max_layers = max(len(top_pair["layers"]), len(control_pair["layers"]))
|
||||
for i in range(max_layers):
|
||||
for c0, pair in ((1, top_pair), (8, control_pair)):
|
||||
if i >= len(pair["layers"]):
|
||||
continue
|
||||
x = pair["layers"][i]
|
||||
ws.cell(r, c0, x["layer"])
|
||||
ws.cell(r, c0 + 1, x.get("label") or f"第{x['layer']}层")
|
||||
ws.cell(r, c0 + 2, x["previous_n"])
|
||||
ws.cell(r, c0 + 3, x["hit_n"])
|
||||
ws.cell(r, c0 + 4, x["probability"])
|
||||
r += 1
|
||||
r += 2
|
||||
|
||||
ws.cell(r, 1, f"汇总·尖子线前{lines['top']['k']}名")
|
||||
ws.cell(r, 8, f"汇总·特控线前{lines['control']['k']}名")
|
||||
r += 1
|
||||
for c0, line_id in ((1, "top"), (8, "control")):
|
||||
ws.cell(r, c0, "分层")
|
||||
ws.cell(r, c0 + 1, "排名区间")
|
||||
ws.cell(r, c0 + 2, "汇总样本")
|
||||
ws.cell(r, c0 + 3, "汇总命中")
|
||||
ws.cell(r, c0 + 4, "合成概率")
|
||||
ws.cell(r, c0 + 5, "拟合概率")
|
||||
_style_header(ws, r, 13)
|
||||
r += 1
|
||||
top_layers = group["summary"]["top"]["layers"]
|
||||
control_layers = group["summary"]["control"]["layers"]
|
||||
for i in range(max(len(top_layers), len(control_layers))):
|
||||
for c0, layers in ((1, top_layers), (8, control_layers)):
|
||||
if i >= len(layers):
|
||||
continue
|
||||
x = layers[i]
|
||||
ws.cell(r, c0, x["layer"])
|
||||
ws.cell(r, c0 + 1, x["label"])
|
||||
ws.cell(r, c0 + 2, x["previous_n"])
|
||||
ws.cell(r, c0 + 3, x["hit_n"])
|
||||
ws.cell(r, c0 + 4, x["probability"])
|
||||
ws.cell(r, c0 + 5, x["fitted_probability"])
|
||||
r += 1
|
||||
r += 1
|
||||
note = (
|
||||
"计算口径:勾选后的每次相邻两次考试都参与比较,最后一次也进入转移样本;同分同名次,后续名次递增;缺考未排除时按0分排名;"
|
||||
"汇总n/k后做加权二项logistic拟合;第1层保留合成概率,首个命中为0的层及其后取0,并强制概率逐层不上升。"
|
||||
)
|
||||
ws.cell(r, 1, note)
|
||||
_borders(ws)
|
||||
ws.column_dimensions["A"].width = 10
|
||||
ws.column_dimensions["H"].width = 10
|
||||
for c in (2, 9):
|
||||
ws.column_dimensions[get_column_letter(c)].width = 24
|
||||
for c in range(3, 8):
|
||||
ws.column_dimensions[get_column_letter(c)].width = 16
|
||||
for c in range(10, 14):
|
||||
ws.column_dimensions[get_column_letter(c)].width = 16
|
||||
return ws
|
||||
|
||||
|
||||
def _metric_sheet(wb: Workbook, group: dict):
|
||||
title = _safe_title(f"{group['group']}指标数")
|
||||
ws = wb.create_sheet(title)
|
||||
r = 1
|
||||
for line in group["targets"]:
|
||||
ws.cell(r, 1, f"{group['group']}方向{line['name']}指标分配")
|
||||
r += 1
|
||||
headers = ["班级"] + [f"{i+1}层人数" for i in range(group["n_layers"])] + ["加权计算", "摊派指标"]
|
||||
ws.cell(r, 1, headers[0])
|
||||
for i, h in enumerate(headers[1:], 2):
|
||||
ws.cell(r, i, h)
|
||||
_style_header(ws, r, len(headers))
|
||||
r += 1
|
||||
ws.cell(r, 1, "排名区间")
|
||||
for i, label in enumerate(line["labels"], 2):
|
||||
ws.cell(r, i, label)
|
||||
r += 1
|
||||
ws.cell(r, 1, "拟合概率")
|
||||
for i, p in enumerate(line["fitted"], 2):
|
||||
ws.cell(r, i, p)
|
||||
r += 1
|
||||
for row in line["classes"]:
|
||||
ws.cell(r, 1, row["class_name"])
|
||||
for i, count in enumerate(row["layer_counts"], 2):
|
||||
ws.cell(r, i, count)
|
||||
ws.cell(r, len(headers) - 1, row["expected"])
|
||||
ws.cell(r, len(headers), row["assigned"])
|
||||
r += 1
|
||||
total_counts = [
|
||||
sum(row["layer_counts"][i] for row in line["classes"])
|
||||
for i in range(group["n_layers"])
|
||||
]
|
||||
ws.cell(r, 1, "合计")
|
||||
for i, count in enumerate(total_counts, 2):
|
||||
ws.cell(r, i, count)
|
||||
ws.cell(r, len(headers) - 1, line["sum_expected"])
|
||||
ws.cell(r, len(headers), line["sum_assigned"])
|
||||
r += 1
|
||||
ws.cell(r, 1, "注:加权计算=Σ(各层人数×拟合概率);摊派指标=加权计算四舍五入。")
|
||||
r += 3
|
||||
ws.column_dimensions["A"].width = 14
|
||||
for c in range(2, ws.max_column + 1):
|
||||
ws.column_dimensions[get_column_letter(c)].width = 13
|
||||
_borders(ws)
|
||||
ws.freeze_panes = "B1"
|
||||
return ws
|
||||
|
||||
|
||||
def build_export(result: dict) -> bytes:
|
||||
wb = Workbook()
|
||||
thin = Border(
|
||||
left=Side(style="thin", color="D9D1C3"),
|
||||
right=Side(style="thin", color="D9D1C3"),
|
||||
top=Side(style="thin", color="D9D1C3"),
|
||||
bottom=Side(style="thin", color="D9D1C3"),
|
||||
)
|
||||
head = PatternFill("solid", fgColor="12233D")
|
||||
head_font = Font(color="F4EFE6", bold=True)
|
||||
wrap = Alignment(wrap_text=True, vertical="center")
|
||||
|
||||
first = True
|
||||
for g in result["groups"]:
|
||||
for t in g["targets"]:
|
||||
title = f"{g['group']}-{t['name']}"[:31]
|
||||
ws = wb.active if first else wb.create_sheet(title)
|
||||
if first:
|
||||
ws.title = title
|
||||
first = False
|
||||
ws.append(
|
||||
["班级", "测算人数", "最近一次在线", "滤波期望", "下达指标", "保底下界", "冲刺上界", "相对最近一次"]
|
||||
)
|
||||
for cell in ws[1]:
|
||||
cell.fill = head
|
||||
cell.font = head_font
|
||||
for row in t["classes"]:
|
||||
ws.append(
|
||||
[
|
||||
row["class_name"],
|
||||
row["n"],
|
||||
row["current"],
|
||||
row["expected"],
|
||||
row["assigned"],
|
||||
row["floor"],
|
||||
row["stretch"],
|
||||
row["delta"],
|
||||
]
|
||||
)
|
||||
ws.append([])
|
||||
ws.append(["说明:下达指标为预估中位水平,班级应努力超越该人数。保底/冲刺来自模拟分布的约20%/80%分位。"])
|
||||
|
||||
ws = wb.create_sheet("学生明细")
|
||||
exams = result["exams"]
|
||||
headers = ["学号", "姓名", "班级", "方向", "学籍", "选科"]
|
||||
headers += [f"{e}-排名" for e in exams]
|
||||
# collect target names
|
||||
tcols = []
|
||||
for g in result["groups"]:
|
||||
for t in g["targets"]:
|
||||
tcols.append((g["group"], t["id"], f"{g['group']}-{t['name']}概率"))
|
||||
headers += [c[2] for c in tcols]
|
||||
headers += ["滤波水平", "预测方差"]
|
||||
ws.append(headers)
|
||||
for cell in ws[1]:
|
||||
cell.fill = head
|
||||
cell.font = head_font
|
||||
cell.alignment = wrap
|
||||
for s in result["students"]:
|
||||
row = [s["sid"], s["name"], s["class_name"], s["group"], s.get("status") or "", s.get("subjects") or ""]
|
||||
ranks = s.get("ranks") or []
|
||||
for i, _e in enumerate(exams):
|
||||
row.append(ranks[i] if i < len(ranks) else None)
|
||||
for gname, tid, _ in tcols:
|
||||
if s["group"] == gname:
|
||||
row.append((s.get("probs") or {}).get(tid))
|
||||
else:
|
||||
row.append(None)
|
||||
row.append(s.get("pred_mean"))
|
||||
row.append(s.get("pred_var"))
|
||||
ws.append(row)
|
||||
wb.remove(wb.active)
|
||||
# Keep the same familiar order as basedata.xlsx: 物理 raw/prob/metric, then 历史.
|
||||
groups = sorted(result["groups"], key=lambda g: _group_order(g["group"]))
|
||||
for i, group in enumerate(groups):
|
||||
students = [s for s in result["students"] if s["group"] == group["group"]]
|
||||
group["_exams"] = result["exams"]
|
||||
_raw_sheet(wb, group, students)
|
||||
for group in groups:
|
||||
_probability_sheet(wb, group)
|
||||
for group in groups:
|
||||
_metric_sheet(wb, group)
|
||||
|
||||
bio = BytesIO()
|
||||
wb.save(bio)
|
||||
|
||||
Reference in New Issue
Block a user