Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2024年秋更新 #157

Merged
merged 2 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions contest/js/static_src/src/index_and_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ if (status !== 'taking contest') {
anchor.addEventListener('click', async (event) => {
// 后续请求异步,必须先阻止
event.preventDefault()

// 如果系统仍有能力,正常答题;不然建议稍后参与
// 为避免进一步向后端施压,这部分逻辑在前端实现,并尽可能拖延时间
if (traffic < 0.90) {
Expand All @@ -52,7 +51,7 @@ if (status !== 'taking contest') {
let interval
await Swal.fire({
title: '非常抱歉',
html: `<p>当前答题人数已达容量 ${(traffic * 100).toFixed()}%,现在答题成绩可能异常。</p><p>建议您等<strong></strong>秒再重新参与。</p>`,
html: '<p>当前答题人数已达上限。</p><p>请等<strong></strong>秒再重新参与。</p>',
icon: 'warning',
timer: TIME_TO_WAIT_IF_JAMMED * 1000, // ms
timerProgressBar: true,
Expand All @@ -63,6 +62,8 @@ if (status !== 'taking contest') {
tick.textContent = `${(Swal.getTimerLeft() / 1000).toFixed()}`
}, 500)
},
allowOutsideClick: false,
allowEscapeKey: false,
willClose: () => clearInterval(interval),
})

Expand Down
3 changes: 3 additions & 0 deletions contest/quiz/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class ConstantsNamespace:
MAX_TRIES = 2
"""答题次数上限"""

MAX_TRAFFIC = 400
"""最大系统承载人数"""

YEAR = 2024
MONTH = 8

Expand Down
2 changes: 1 addition & 1 deletion contest/quiz/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def manage_status(
def calc_traffic() -> float:
"""估计当前在线人数占系统能力的比例"""
# TODO: 此处是经验公式,应该按实际情况更新
return DraftResponse.objects.count() / 400
return DraftResponse.objects.count() / constants.MAX_TRAFFIC


class IndexView(TemplateView):
Expand Down
74 changes: 56 additions & 18 deletions doc/agg.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,43 @@ $ poetry install --with agg

## 流程

在`just shell`中运行以下内容,定稿所有超期答卷。
### 从服务器导出

```shell
在`just shell`中运行以下内容(运行`just shell`,然后粘贴进去),定稿所有超期答卷。

```python
from quiz.models import DraftResponse
from quiz.views import continue_or_finalize

for r in DraftResponse.objects.all():
print(continue_or_finalize(r))
```

利用`just manage dumpdata`和`just manage loaddata`,将数据复制到本地。(数据量大,而服务器资源有限)
利用`just manage dumpdata`导出数据,然后下载到本地。(数据量大,而服务器资源有限)

```shell
$ just manage dumpdata quiz contenttypes auth --format jsonl --output db.jsonl
```

> [!NOTE]
>
> - 这里只导出了`quiz`相关数据,文件会小几 MB。
> - JSON lines(`*.jsonl`)格式每行一条记录,方便检查。

### 在本地统计

清空本地数据库再创建空表,然后导入下载下来的数据。

```shell
$ rm ./contest/db.sqlite3 && just manage migrate
$ just manage loaddata db.jsonl --verbosity 3
```

再在`just shell`中运行以下内容,导出分数为`scores.csv`。

```python
"""导出分数"""

from collections import deque
from pathlib import Path

Expand All @@ -40,35 +61,46 @@ for s in track(
):
data.append((s.user.username, s.user.last_name, str(s.final_score())))

#! EDIT HERE: scores.csv 的导出路径
Path("scores.csv").write_text("\n".join(map(",".join, data)), encoding="utf-8")
```

然后进一步计算每连情况。

```python
"""计算每连情况"""

from pathlib import Path

import polars as pl

# 1. Load data and verify

scores_path = next(Path(__file__).parent.glob("scores*.csv"))
#! EDIT HERE: scores.csv 的路径,由上一步导出
scores_path = next(Path.cwd().glob("scores*.csv"))
print(f"分析“{scores_path}”。")

scores = pl.scan_csv(
scores_path,
schema={"id": pl.Utf8, "name": pl.Utf8, "score": pl.Int16},
)

people = pl.read_excel(
Path("D:/大学/Clubs/NetPioneer_2022_2023/技术保障中心/国防知识竞赛/连队人员信息0830.xlsx"),
read_csv_options={
"schema": {"营团": pl.Utf8, "连队": pl.Int64, "id": pl.Utf8, "name": pl.Utf8}
},
).join(scores.collect(), on="id", how="outer")
people = (
pl.read_excel(
#! EDIT HERE: 学生名单的路径,由学工部提供
Path("2024-08-20军训学生分连队.xlsx"),
read_csv_options={
#! EDIT HERE: 按顺序记录名单格式,多余的列可在后面 drop
"schema": {"序号": pl.Utf8, "id": pl.Utf8, "name": pl.Utf8, "连队": pl.Utf8}
},
)
.drop("序号")
# 解析 "1连" → 1
.with_columns(pl.col("连队").str.strip_suffix("连").cast(pl.Int16))
.join(scores.collect(), on="id", how="outer")
)

print("名单没有的同学:", people.filter(pl.col("name").is_null()))
print("名单没有的同学:", people.filter(pl.col("name").is_null()).sort("id"))

inconsistent = people.filter(
pl.col("name").is_not_null()
Expand All @@ -81,7 +113,7 @@ people = (
people.lazy()
.with_columns(pl.col("name").fill_null(pl.col("name_right")))
.drop("name_right")
.filter(pl.col("营团").is_not_null() & pl.col("连队").is_not_null())
.filter(pl.col("连队").is_not_null())
.collect()
)

Expand All @@ -91,17 +123,23 @@ print("名单中同学:", people.describe())

q = (
people.lazy()
.group_by("营团", "连队")
#! EDIT HERE: 若分了营团,可考虑改为 group_by("营团", "连队") 和 sort("营团", "连队")
.group_by("连队")
.agg(
pl.count().alias("总人数"),
pl.col("score").fill_null(0).mean().alias("平均分"),
(pl.col("score") > 0).sum().alias("答题人数"),
pl.count().alias("应参与人数"),
(pl.col("score") > 0).sum().alias("实际参与人数"),
#! EDIT HERE: 需要统计的数据
(pl.col("score").fill_null(0) > 0).mean().alias("参与率"),
pl.col("score").fill_null(0).sum().alias("总得分"),
pl.col("score").fill_null(0).mean().alias("均分"),
pl.col("score").filter(pl.col("score") > 0).mean().alias("有成绩学生平均分"),
)
.with_columns((pl.col("答题人数") / pl.col("总人数")).alias("答题比例"))
.sort("营团", "连队")
.sort("连队")
.with_columns(pl.col("连队").cast(pl.Utf8) + "连")
)
df = q.collect()
print("各连情况:", df.describe())
#! EDIT HERE: 统计结果的导出路径
agg_path = scores_path.with_name(f"{scores_path.stem}-agg.xlsx")
df.write_excel(agg_path, column_formats={"答题比例": "0.00%"})
print(f"已保存到“{agg_path}”。")
Expand Down