Files
hangbiao/app/export_xlsx.py
2026-09-26 19:21:16 +08:00

212 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from io import BytesIO
from openpyxl import Workbook
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()
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)
return bio.getvalue()