first commit
This commit is contained in:
263
app/templates/analysis.html
Normal file
263
app/templates/analysis.html
Normal file
@@ -0,0 +1,263 @@
|
||||
<!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 %}
|
||||
<a class="ghost" href="/analysis/{{ item.id }}/export">导出 Excel</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">先确认系统读到的是你想算的那批人。分数量纲可以不同,系统稍后会在每次考试内部标准化。</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.art }} / {{ summary.leave }}</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 sh in summary.sheets %}
|
||||
<tr><td>{{ sh.name }}</td><td>{{ sh.kind }}</td><td>{{ sh.rows }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>方向 / 学籍</h3>
|
||||
<div class="chips">
|
||||
{% for k,v in summary.tracks.items() %}<span class="chip">{{ k or "未分方向" }} {{ v }}</span>{% endfor %}
|
||||
</div>
|
||||
<div class="chips">
|
||||
{% for k,v in summary.status.items() %}<span class="chip">{{ k }} {{ v }}</span>{% endfor %}
|
||||
</div>
|
||||
</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">平均分从高一到高三明显下降是正常的(试卷结构不同),所以不能直接比原始分,只能比同次考试里的相对位置。</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3>各班人数</h3>
|
||||
<div class="chips">
|
||||
{% for k,v in summary.classes.items() %}<span class="chip">{{ k }}班 {{ v }}人</span>{% endfor %}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="primary" href="/analysis/{{ item.id }}?step=2">口径看起来对,下一步</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if step == 2 %}
|
||||
<h1>第二步:口径与目标线</h1>
|
||||
<p class="muted">下达指标前先把“算谁、瞄哪条线”钉死。默认排除艺体和长期请假,明远生可参与年级排名。</p>
|
||||
<form method="post" action="/analysis/{{ item.id }}/settings" onsubmit="if(document.activeElement && document.activeElement.value==='compute'){this.querySelector('[value=compute]').textContent='正在滤波测算…'; this.querySelector('[value=compute]').disabled=false;}">
|
||||
<h3>用于滤波的考试</h3>
|
||||
<p class="tiny">建议全选。系统会自动让近考权重大、早考权重小;不是把五次考试简单平均。</p>
|
||||
{% 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:12px">
|
||||
<div>
|
||||
<h3>纳入测算</h3>
|
||||
<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>
|
||||
<label class="check"><input type="checkbox" name="include_art" {% if settings.include_art %}checked{% endif %}> 包含艺体生</label>
|
||||
<label class="check"><input type="checkbox" name="include_leave" {% if settings.include_leave %}checked{% endif %}> 包含请假 / 出国 / 休学备注</label>
|
||||
{% if preview_n is not none %}<p class="tiny">按当前口径约 {{ preview_n }} 人进入测算。</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h3>预估多远</h3>
|
||||
<label>预测步长</label>
|
||||
<select name="horizon">
|
||||
<option value="1" {% if settings.horizon == 1 %}selected{% endif %}>下一次大考(1 步)</option>
|
||||
<option value="2" {% if settings.horizon == 2 %}selected{% endif %}>大约高考前再考 2 次的距离</option>
|
||||
</select>
|
||||
<p class="tiny">步长越远,不确定性越大,临界生会变多,下达人数仍按期望取整到目标总量。</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3>各方向目标人数</h3>
|
||||
<p class="tiny">这是年级总盘子。班级指标由个人达线概率加总后再取整,合计等于这里的人数。</p>
|
||||
{% for g in settings.groups %}
|
||||
<div class="explain" style="margin-bottom:12px">
|
||||
<b>{{ g }}</b>
|
||||
{% for t in settings.targets[g] %}
|
||||
<div class="grid-2" style="margin-top:8px">
|
||||
<input type="hidden" name="t_id_{{ g }}" value="{{ t.id }}">
|
||||
<div><label>指标名称</label><input name="t_name_{{ g }}" value="{{ t.name }}"></div>
|
||||
<div><label>年级总人数</label><input name="t_k_{{ g }}" type="number" min="1" value="{{ t.k }}"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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>
|
||||
{% else %}
|
||||
<div class="route">
|
||||
<i>原始分</i><i class="alt">同次标准化</i><i>卡尔曼估计水平</i><i class="alt">模拟下一次排名</i><i>个人概率</i><i class="alt">班级加总取整</i>
|
||||
</div>
|
||||
<p class="explain">
|
||||
把每次考试看成一次带噪声的定位:发挥好坏是测量噪声 R,真实进步是过程噪声 Q。
|
||||
R 相对大时,系统不会只信最近一次;Q 相对大时,会更快跟上近期趋势。这就是导航里“既看 GPS,也看车自己开的方向”。
|
||||
</p>
|
||||
{% for g in result.groups %}
|
||||
<h3>{{ g.group }} · 模型参数</h3>
|
||||
<div class="kpis">
|
||||
<div class="kpi"><em>过程噪声 Q(真实水平会变)</em><strong>{{ g.model.q }}</strong></div>
|
||||
<div class="kpi"><em>测量噪声 R(发挥波动)</em><strong>{{ g.model.r }}</strong></div>
|
||||
<div class="kpi"><em>一步预测 RMSE</em><strong>{{ g.model.rmse }}</strong></div>
|
||||
<div class="kpi"><em>测算人数 / 模拟次数</em><strong>{{ g.n }} / {{ g.model.n_sim }}</strong></div>
|
||||
</div>
|
||||
{% if g.holdout %}
|
||||
<p class="tiny">留出最近一次考试做对照:用更早的成绩预估最近一次,看滤波是否比“只看上一场名次”更准。</p>
|
||||
<table class="table">
|
||||
<thead><tr><th>目标</th><th>K</th><th>滤波命中</th><th>只看最近一次</th></tr></thead>
|
||||
<tbody>
|
||||
{% for h in g.holdout %}
|
||||
<tr>
|
||||
<td>{{ h.target_name }}</td>
|
||||
<td>{{ h.k }}</td>
|
||||
<td>{{ h.kf_hit }} / {{ h.k }}({{ '%.0f'|format(h.kf_precision * 100) }}%)</td>
|
||||
<td>{{ h.last_hit }} / {{ h.k }}({{ '%.0f'|format(h.last_precision * 100) }}%)</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
<p class="muted">还没有测算结果。请先完成第二步测算。</p>
|
||||
{% else %}
|
||||
<p class="explain">
|
||||
「下达指标」是滤波期望取整后的人数,含义是:若学生按现有轨迹走,这个班本来应当到线的人数。
|
||||
班会目标是超越它,而不是刚好打平。保底 / 冲刺是模拟分布的大约 20% / 80% 分位,用来看把握而不是另下两套考核。
|
||||
</p>
|
||||
{% for g in result.groups %}
|
||||
{% for t in g.targets %}
|
||||
<h3>{{ g.group }} · {{ t.name }}(年级 {{ t.k }} 人)</h3>
|
||||
<p class="tiny">期望合计 {{ t.sum_expected }},临界生(概率 20%–80%){{ t.critical_n }} 人。点击班级看学生概率,点击学生看轨迹。</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>班级</th><th>测算人数</th><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>{{ row.floor }}</td>
|
||||
<td>{{ row.stretch }}</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="8" class="tiny">{{ class_comment(row, t.name) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if t.critical %}
|
||||
<h4>临界生(优先关注)</h4>
|
||||
<table class="table">
|
||||
<thead><tr><th>姓名</th><th>班</th><th>最近名次</th><th>达线概率</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in t.critical[:15] %}
|
||||
<tr class="clickable" onclick="openStudent({{ item.id }}, '{{ s.sid }}')">
|
||||
<td>{{ s.name }}</td><td>{{ s.class_name }}</td>
|
||||
<td>{{ s.last_rank or "—" }}</td>
|
||||
<td><span class="tag mid">{{ '%.1f'|format(s.p * 100) }}%</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
55
app/templates/home.html
Normal file
55
app/templates/home.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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>班级达线指标预估系统</span></div>
|
||||
</a>
|
||||
<form action="/logout" method="post"><button class="ghost" type="submit">退出 {{ user }}</button></form>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<h1>把历史成绩当成导航轨迹,<br>而不是一张静态排名表。</h1>
|
||||
<p>
|
||||
上传各次大考总分后,系统会把分数换成同一次考试内的标准分,用卡尔曼滤波估计每个学生的稳定水平,
|
||||
再模拟下一次全年级排名,把达线概率加总到班级。下达的是“按现有轨迹本来会到的人数”,班级工作是超越它。
|
||||
</p>
|
||||
</div>
|
||||
<form class="drop" action="/upload" method="post" enctype="multipart/form-data">
|
||||
<h3>上传成绩表</h3>
|
||||
<p class="muted">xlsx,需含学号/姓名/班级,以及多次考试总分列。若有「方向」或物理/历史分表,会分开测算。</p>
|
||||
<input type="file" name="file" accept=".xlsx,.xlsm" required>
|
||||
<div class="actions">
|
||||
<button class="primary" type="submit">开始逐步分析</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% if has_sample %}
|
||||
<form action="/demo" method="post" style="margin-top:12px">
|
||||
<button class="btn teal" type="submit">用文件夹里的 2024 级示例表走一遍</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="list">
|
||||
{% for it in items %}
|
||||
<a class="item" href="/analysis/{{ it.id }}?step={{ it.step or 1 }}">
|
||||
<div>
|
||||
<b>{{ it.title }}</b>
|
||||
<div class="tiny">{{ it.filename }} · {{ it.created_at }}</div>
|
||||
</div>
|
||||
<span class="chip">继续第 {{ it.step or 1 }} 步</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="muted">还没有分析记录。上传成绩表后会在这里列出。</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
25
app/templates/login.html
Normal file
25
app/templates/login.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>登录 · 航标</title>
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrap">
|
||||
<form class="login-card" method="post" action="/login">
|
||||
<div class="mark">航</div>
|
||||
<h1>航标</h1>
|
||||
<p class="muted">用历史成绩滤波,预估各班后续达线指标。管理员登录后上传成绩表,逐步确认口径,再下达可超越的班级目标。</p>
|
||||
<label>账号</label>
|
||||
<input name="username" value="admin" autocomplete="username">
|
||||
<label>密码</label>
|
||||
<input name="password" type="password" autocomplete="current-password">
|
||||
{% if error %}<div class="err">{{ error }}</div>{% endif %}
|
||||
<div class="actions"><button class="primary" type="submit">进入系统</button></div>
|
||||
<p class="tiny">默认账号 admin / hangbiao2026,请部署后尽快修改 app/config.py。</p>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user