使用梅校的算法

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

@@ -32,9 +32,9 @@ from .db import (
)
from .excel_parse import parse_workbook, summarize_parse
from .export_xlsx import build_export
from .indicators import analyze, class_comment, default_targets, infer_track_group, select_students
from .layer_method import analyze, class_comment, default_line_settings, infer_track_group, select_students
app = FastAPI(title="航标 · 班级达线指标预估")
app = FastAPI(title="指标测算 · 相邻层次法")
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, max_age=60 * 60 * 12)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
@@ -136,17 +136,20 @@ def demo(request: Request):
def _settings_from(parsed: dict, stored: dict | None) -> dict:
summary = summarize_parse(parsed)
groups = sorted({infer_track_group(s["track"]) for s in parsed["students"]})
line_settings = default_line_settings(groups)
base = {
"exams": parsed["exams"],
"include_art": False,
"include_leave": False,
"exclude_moves": False,
"include_mingyuan": True,
"include_other": True,
"horizon": 1,
"targets": default_targets(groups),
"line_settings": line_settings,
}
if stored:
for k in ("exams", "include_art", "include_leave", "include_mingyuan", "include_other", "horizon", "targets"):
# 考试列必须始终来自本次上传的工作簿,避免旧分析设置把不存在的列带进来。
valid_exam_set = set(parsed["exams"])
stored_exams = [e for e in (stored.get("exams") or []) if e in valid_exam_set]
base["exams"] = stored_exams or parsed["exams"]
for k in ("exclude_moves", "include_mingyuan", "include_other", "line_settings"):
if k in stored:
base[k] = stored[k]
base["summary"] = summary
@@ -154,6 +157,7 @@ def _settings_from(parsed: dict, stored: dict | None) -> dict:
return base
@app.get("/analysis/{aid}", response_class=HTMLResponse)
def analysis_page(request: Request, aid: int, step: int = 1):
if (redir := require_login(request)):
@@ -189,34 +193,39 @@ async def save_settings(request: Request, aid: int):
return HTMLResponse("分析不存在", status_code=404)
parsed = load_json(item["parsed_path"])
form = await request.form()
exams = form.getlist("exams")
valid_exam_set = set(parsed["exams"])
exams = [e for e in form.getlist("exams") if e in valid_exam_set]
if not exams:
exams = parsed["exams"]
if len(exams) < 2:
return HTMLResponse("至少需要勾选两次考试才能计算相邻转移概率。", status_code=400)
groups = sorted({infer_track_group(s["track"]) for s in parsed["students"]})
targets = {}
defaults = default_line_settings(groups)
line_settings = {}
for g in groups:
names = form.getlist(f"t_name_{g}")
ks = form.getlist(f"t_k_{g}")
ids = form.getlist(f"t_id_{g}")
rows = []
for i, name in enumerate(names):
k_raw = ks[i] if i < len(ks) else "0"
try:
k = int(float(k_raw))
except ValueError:
k = 0
tid = ids[i] if i < len(ids) and ids[i] else f"t{i+1}"
if name.strip() and k > 0:
rows.append({"id": tid, "name": name.strip(), "k": k})
targets[g] = rows or default_targets([g])[g]
try:
top = int(float(form.get(f"top_k_{g}", defaults[g]["top"])))
except ValueError:
top = defaults[g]["top"]
try:
control = int(float(form.get(f"control_k_{g}", defaults[g]["control"])))
except ValueError:
control = defaults[g]["control"]
try:
layer_size = int(float(form.get(f"layer_size_{g}", defaults[g]["layer_size"])))
except ValueError:
layer_size = defaults[g]["layer_size"]
line_settings[g] = {
"top": max(1, top),
"control": max(1, max(top, control)),
"layer_size": max(1, layer_size),
}
settings = {
"exams": list(exams),
"include_art": form.get("include_art") == "on",
"include_leave": form.get("include_leave") == "on",
"exclude_moves": form.get("exclude_moves") == "on",
"include_mingyuan": form.get("include_mingyuan") == "on",
"include_other": form.get("include_other") == "on",
"horizon": int(form.get("horizon") or 1),
"targets": targets,
"line_settings": line_settings,
}
update_analysis(aid, settings_json=json.dumps(settings, ensure_ascii=False), step=2)
action = form.get("action") or "next"
@@ -230,6 +239,7 @@ async def save_settings(request: Request, aid: int):
return RedirectResponse(f"/analysis/{aid}?step=2", status_code=303)
@app.get("/analysis/{aid}/api/student/{sid}")
def student_api(request: Request, aid: int, sid: str):
if not current_user(request):
@@ -257,8 +267,14 @@ def class_api(request: Request, aid: int, group: str, target: str, class_name: s
for s in result["students"]
if s["group"] == group and s["class_name"] == class_name
]
rows.sort(key=lambda s: -((s.get("probs") or {}).get(target) or 0))
return {"exams": result["exams"], "students": rows[:80], "n": len(rows)}
rows.sort(key=lambda s: (s.get("ranks") or {}).get(result["exams"][-1], 10**9))
return {
"exams": result["exams"],
"layer_size": result.get("layer_size"),
"last_exam": result["exams"][-1],
"students": rows,
"n": len(rows),
}
@app.get("/analysis/{aid}/export")
@@ -269,6 +285,8 @@ def export_analysis(request: Request, aid: int):
if not item or not item.get("result_path"):
return HTMLResponse("请先完成测算", status_code=400)
result = load_json(item["result_path"])
if result.get("algorithm") != "adjacent-layer-markov-v2":
return HTMLResponse("结果为旧算法,请回到第二步重新测算后再导出。", status_code=400)
data = build_export(result)
ascii_name = f"hangbiao-{aid}.xlsx"
star = quote(f"{item['title']}-下达指标.xlsx")