Files
hangbiao/app/templates/analysis.html
2026-09-26 19:21:16 +08:00

251 lines
13 KiB
HTML
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.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ item.title }} · 相邻层次法</title>
<link rel="stylesheet" href="/static/css/app.css">
</head>
<body>
<header class="topbar">
<a class="brand" href="/">
<div class="mark">层</div>
<div><b>指标测算</b><span>{{ item.title }}</span></div>
</a>
<div style="display:flex;gap:8px">
{% if result and result.get('algorithm') == 'adjacent-layer-markov-v2' %}
<a class="ghost" href="/analysis/{{ item.id }}/export">导出总表</a>
{% endif %}
<form action="/logout" method="post"><button class="ghost" type="submit">退出</button></form>
</div>
</header>
<main class="wrap layout">
<aside class="rail">
<h2>测算航线</h2>
{% set steps = [
(1, '数据体检', '确认学生、考试、缺考'),
(2, '口径与线', '排除异动、改尖子/特控线'),
(3, '相邻命中概率', '逐对考试汇总并拟合'),
(4, '指标摊派', '最后一次成绩分班下达')
] %}
{% for n, title, desc in steps %}
<a class="step {% if step == n %}on{% endif %}" href="/analysis/{{ item.id }}?step={{ n }}">
<div class="dot">{{ n }}</div>
<div><b>{{ title }}</b><span>{{ desc }}</span></div>
</a>
{% endfor %}
</aside>
<section class="card panel">
{% if step == 1 %}
<h1>第一步:成绩表体检</h1>
<p class="muted">这里只检查源表识别结果。缺考学生在后续测算中会按 0 分参加同方向排名。</p>
<div class="kpis">
<div class="kpi"><em>识别学生</em><strong>{{ summary.n }}</strong></div>
<div class="kpi"><em>班级</em><strong>{{ summary.n_class }}</strong></div>
<div class="kpi"><em>考试次数</em><strong>{{ summary.exams|length }}</strong></div>
<div class="kpi"><em>异动备注</em><strong>{{ summary.moves }}</strong></div>
</div>
{% for w in summary.warnings %}
<div class="warn">{{ w }}</div>
{% endfor %}
<div class="grid-2">
<div>
<h3>工作表</h3>
<table class="table">
<thead><tr><th>名称</th><th>类型</th><th>行数</th></tr></thead>
<tbody>
{% for s in summary.sheets %}
<tr><td>{{ s.name }}</td><td>{{ s.kind }}</td><td>{{ s.rows }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div>
<h3>考试识别</h3>
<table class="table">
<thead><tr><th>考试</th><th>有分人数</th><th>最低</th><th>最高</th><th>平均</th></tr></thead>
<tbody>
{% for e in summary.score_scale %}
<tr><td>{{ e.exam }}</td><td>{{ e.n }}</td><td>{{ e.min }}</td><td>{{ e.max }}</td><td>{{ e.mean }}</td></tr>
{% endfor %}
</tbody>
</table>
<p class="tiny">有分人数不等于测算人数:未排除的异动学生若缺考,仍会按 0 分进入排名。</p>
</div>
</div>
<div class="actions"><a class="primary" href="/analysis/{{ item.id }}?step=2">下一步设置</a></div>
{% endif %}
{% if step == 2 %}
<h1>第二步:口径、层次与线</h1>
<form method="post" action="/analysis/{{ item.id }}/settings">
<h3>考试范围</h3>
<p class="tiny">考试列只来自本次上传工作簿的物理/历史人员表;只比较这里勾选后的相邻两次考试。例如勾选 7 次,会生成 6 组转移样本;最后一次既进入相邻概率计算,也用于最终摊派。</p>
<div class="explain" style="margin-bottom:10px"><b>本次源表</b>:{{ item.filename }};方向人员表识别到 {{ summary.exams|length }} 个考试列。</div>
{% for e in summary.exams %}
<label class="check"><input type="checkbox" name="exams" value="{{ e }}" {% if e in settings.exams %}checked{% endif %}> {{ e }}(有分 {{ summary.exam_present[e] }} 人)</label>
{% endfor %}
<div class="grid-2" style="margin-top:16px">
<div>
<h3>纳入范围</h3>
<label class="check"><input type="checkbox" name="exclude_moves" {% if settings.exclude_moves %}checked{% endif %}> 排除异动学生</label>
<p class="tiny">勾选后,「异动/备注」列非空的学生整行不参与排名和概率计算;不勾选则纳入,且缺考按 0 分排名。</p>
<label class="check"><input type="checkbox" name="include_mingyuan" {% if settings.include_mingyuan %}checked{% endif %}> 包含明远 / 港籍学籍</label>
<label class="check"><input type="checkbox" name="include_other" {% if settings.include_other %}checked{% endif %}> 包含其他学籍</label>
{% if preview_n is not none %}<p class="tiny">当前口径进入测算 {{ preview_n }} 人。</p>{% endif %}
</div>
<div>
<h3>计算规则</h3>
<p class="explain">
同分数为同名次,后续名次递增。层次边界遇到同分时整组留在上一层;
下一层从下一个不同名次开始,不会把边界同分拆到两组。
</p>
</div>
</div>
<h3>各方向线与层次宽度</h3>
<p class="tiny">线按“方向内名次”设置,可在下方直接修改。历史方向默认按 basedata.xlsx 使用 5 人一层;物理默认 20 人一层。</p>
{% for g in settings.groups %}
<div class="explain" style="margin-bottom:12px">
<b>{{ g }}方向</b>
<div class="grid-3" style="margin-top:8px">
<div><label>尖子线人数</label><input name="top_k_{{ g }}" type="number" min="1" value="{{ settings.line_settings[g].top }}"></div>
<div><label>特控线人数</label><input name="control_k_{{ g }}" type="number" min="1" value="{{ settings.line_settings[g].control }}"></div>
<div><label>每层人数</label><input name="layer_size_{{ g }}" type="number" min="1" value="{{ settings.line_settings[g].layer_size }}"></div>
</div>
</div>
{% endfor %}
<div class="actions">
<button class="ghost" name="action" value="save">只保存口径</button>
<button class="primary" name="action" value="compute">开始计算概率与摊派</button>
</div>
</form>
{% endif %}
{% if step == 3 %}
<h1>第三步:相邻命中概率与拟合</h1>
{% if not result %}
<p class="muted">还没有测算结果。请回到第二步点击测算。</p>
<a class="primary" href="/analysis/{{ item.id }}?step=2">去设置</a>
{% elif result.get('algorithm') != 'adjacent-layer-markov-v2' %}
<div class="warn">这是旧算法结果。请回到第二步重新测算一次。</div>
{% else %}
<div class="kpis">
<div class="kpi"><em>进入测算</em><strong>{{ result.n_selected }} 人</strong></div>
<div class="kpi"><em>缺考处理</em><strong>0 分排名</strong></div>
<div class="kpi"><em>异动排除</em><strong>{{ '是' if result.exclude_moves else '否' }}</strong></div>
<div class="kpi"><em>相邻对数</em><strong>{{ result.pair_count }}</strong></div>
</div>
{% for g in result.groups %}
<div class="explain" style="margin-top:18px">
<b>{{ g.group }}方向</b>:{{ g.n }} 人,{{ g.layer_size }} 名一层,共 {{ g.n_layers }} 层;
尖子线前 {{ g.line_settings.top }} 名,特控线前 {{ g.line_settings.control }} 名。
</div>
<h3>汇总概率与拟合概率</h3>
<p class="tiny">拟合规则与 basedata.xlsx 保持一致:第 1 层保留合成概率;中间层用加权 logistic 拟合;首个命中为 0 的层及其后取 0。最后一次考试也已纳入相邻转移计算。</p>
<div class="grid-2">
{% for line_id in ['top', 'control'] %}
{% set sm = g.summary[line_id] %}
<div>
<h4>{{ '尖子线' if line_id == 'top' else '特控线' }}</h4>
<table class="table">
<thead><tr><th>前一次层次</th><th>样本 n</th><th>命中 k</th><th>合成概率</th><th>拟合概率</th></tr></thead>
<tbody>
{% for x in sm.layers %}
<tr>
<td>{{ x.label }}</td><td>{{ x.previous_n }}</td><td>{{ x.hit_n }}</td>
<td>{{ '%.2f'|format(x.probability * 100) if x.probability is not none else '—' }}</td>
<td><b>{{ '%.2f'|format(x.fitted_probability * 100) }}</b></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div>
<h3>每次相邻比较明细</h3>
{% for line in g.pairs %}
{% for pair in line.pairs %}
<details>
<summary><b>{{ line.line }}</b>:{{ pair.from_exam }} → {{ pair.to_exam }}</summary>
<div style="overflow:auto">
<table class="table">
<thead><tr><th>前一次层次</th><th>人数</th><th>下次到线</th><th>概率</th></tr></thead>
<tbody>
{% for x in pair.layers %}
<tr><td>{{ x.label or x.layer }}</td><td>{{ x.previous_n }}</td><td>{{ x.hit_n }}</td>
<td>{{ '%.2f'|format(x.probability * 100) if x.probability is not none else '—' }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</details>
{% endfor %}
{% endfor %}
{% endfor %}
<div class="actions"><a class="primary" href="/analysis/{{ item.id }}?step=4">看指标摊派</a></div>
{% endif %}
{% endif %}
{% if step == 4 %}
<h1>第四步:最后一次成绩摊派</h1>
{% if not result or result.get('algorithm') != 'adjacent-layer-markov-v2' %}
<p class="muted">还没有新算法结果。请回第二步重新测算。</p>
<a class="primary" href="/analysis/{{ item.id }}?step=2">去设置</a>
{% else %}
<p class="explain">按最后一次考试的各班层次人数,乘以汇总后的拟合概率;加权计算四舍五入为摊派指标。点击班级可查看学生最后一次名次。</p>
{% for g in result.groups %}
{% for t in g.targets %}
<h3>{{ g.group }}方向 · {{ t.name }}(前 {{ t.k }} 名)</h3>
<p class="tiny">年级最后一次在线 {{ t.sum_current }} 人;加权合计 {{ t.sum_expected }};摊派合计 {{ t.sum_assigned }} 人。</p>
<table class="table">
<thead><tr><th>班级</th><th>测算人数</th><th>最后在线</th><th>加权计算</th><th>摊派指标</th><th>相对最后在线</th></tr></thead>
<tbody>
{% for row in t.classes %}
<tr class="clickable" onclick="openClass({{ item.id }}, '{{ g.group }}', '{{ t.id }}', '{{ row.class_name }}')">
<td>{{ row.class_name }}班</td><td>{{ row.n }}</td><td>{{ row.current }}</td>
<td>{{ '%.2f'|format(row.expected) }}</td><td><b>{{ row.assigned }}</b></td>
<td class="{% if row.delta > 0 %}delta-up{% elif row.delta < 0 %}delta-down{% endif %}">{% if row.delta > 0 %}+{{ row.delta }}{% else %}{{ row.delta }}{% endif %}</td>
</tr>
<tr><td colspan="6" class="tiny">{{ class_comment(row, t.name) }}</td></tr>
{% endfor %}
<tr><th>合计</th><th>{{ g.n }}</th><th>{{ t.sum_current }}</th><th>{{ t.sum_expected }}</th><th>{{ t.sum_assigned }}</th><th>—</th></tr>
</tbody>
</table>
<details>
<summary>查看班级层次分布</summary>
<div style="overflow:auto">
<table class="table">
<thead><tr><th>班级</th>{% for label in t.labels %}<th>{{ label }}</th>{% endfor %}</tr></thead>
<tbody>
{% for row in t.classes %}
<tr><td>{{ row.class_name }}班</td>{% for c in row.layer_counts %}<td>{{ c }}</td>{% endfor %}</tr>
{% endfor %}
<tr><td><b>拟合概率</b></td>{% for p in t.fitted %}<td>{{ '%.2f'|format(p * 100) }}</td>{% endfor %}</tr>
</tbody>
</table>
</div>
</details>
{% endfor %}
{% endfor %}
<div class="actions">
<a class="primary" href="/analysis/{{ item.id }}/export">导出下载总表</a>
<a class="ghost" href="/analysis/{{ item.id }}?step=2">调整线再算</a>
</div>
{% endif %}
{% endif %}
</section>
</main>
<div class="drawer-bg" id="drawer"><div class="drawer" id="drawer-body"></div></div>
<script>window.HB_EXAMS = {{ (result.exams if result else summary.exams) | tojson }};</script>
<script src="/static/js/app.js"></script>
</body>
</html>