使用梅校的算法

This commit is contained in:
2026-09-26 19:21:16 +08:00
parent d9ff32d2bb
commit eeae5f9d68
12 changed files with 846 additions and 351 deletions

View File

@@ -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)