251 lines
13 KiB
HTML
251 lines
13 KiB
HTML
<!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>
|