国家学生体质健康标准(2014修订)评分引擎
Goal
给定一个学生的原始体测测量值,按指定年级 + 性别,依据《国家学生体质健康标准(2014年修订)》算出:
每项得分、等级、跳绳附加分、标准分、总分、总等级,以及 BMI 及其等级。全项齐时与 Excel 中「名称管理器 + VLOOKUP(近似匹配 TRUE)」的实现逐分等价。
缺项按「有多少项评多少项」处理:项目无成绩(该年级国标不测 / 学校未录 / 该生缺考)自动不参与计分,标准分 = Σ(得分×权重) ÷ Σ(有成绩项权重),即已测项目的加权平均分(100 分制);任何项目都无成绩才不出分。
支持范围
| 学段 | 年级 | 状态 |
|---|---|---|
| 小学 | 一年级 ~ 六年级 | ✅ 已支持 |
| 初中 | 初一 / 初二 / 初三 | ✅ 已支持 |
| 高中 | 高一 / 高二 / 高三 | ✅ 已支持 |
查询当前支持范围:
from score_engine_2014 import list_grades, grade_groups, describe_grade
list_grades() # 12 个年级:一年级…六年级、初一…初三、高一…高三
grade_groups() # {'小学': [...6], '初中': [...3], '高中': [...3]}
describe_grade('初一') # 该年级的项目/权重/时间类项目/附加分项/BMI边界
各学段项目差异(引擎按数据自动适配,传多余项目会被忽略):
| 学段 | 项目 |
|---|---|
| 一、二年级 | BMI、肺活量、50米跑、坐位体前屈、一分钟跳绳 |
| 三、四年级 | 上述 + 仰卧起坐 |
| 五、六年级 | 上述 + 50米×8折返跑 |
| 初、高中 | BMI、肺活量、50米跑、立定跳远、坐位体前屈、耐力跑(男1000米/女800米)、仰卧引体(男引体向上/女一分钟仰卧起坐) |
为什么有这个 skill
原始 Excel 算分表用 Sheets 的名称管理器定义了一整套评分公式,核心是:
VLOOKUP(原始值, {阈值表}, 2, TRUE) # TRUE = 升序近似匹配
每个年级、每个项目、男女各一张阈值表。这套逻辑可以纯 Python 复刻,做到 100% 还原,且不依赖 Excel / openpyxl / 宏,可在任意环境直接调用,方便跨项目迁移、批量化、嵌入到其他系统。
数据文件
standard2014_data.json—— 从官方 xlsm 的名称管理器自动提取的全年级全套 2014 标准阈值表(小学一~六年级 + 初中初一~初三 + 高中高一~高三,各项目 × 男/女 + BMI 边界 + 附加分规则 + 权重)。- 40 张时间类阈值表已内置 0 值保护档(成绩 0 一律得 0 分,不误判满分)。
- 2 处标准阈值已校准(四年级跳绳女、初三体前屈男)。
score_engine_2014.py—— 评分引擎,智能自动定位同目录下的 json。
快速使用
import sys, os
sys.path.insert(0, os.path.dirname(__file__)) # 若引擎不在当前目录,指向 skill 里的 score_engine_2014.py 所在目录
from score_engine_2014 import score_one
# 单个男生(六年级):items 用身高/体重 + 6项实测值
items = {
"身高": 149, "体重": 51.3,
"肺活量": 1219, "50米跑": 10.09, "坐位体前屈": 4,
"一分钟跳绳": 140, "仰卧起坐": 21, "折返跑": 2.19,
}
res = score_one("六年级", 1, items=items) # 1=男, 2=女(也兼容 '男'/'女')
print(res["bmi"], res["bmi_label"], res["bmi_score"]) # 23.107 '超重' 80
print(res["scores"]) # {'BMI':80,'肺活量':20,'50米跑':64,'坐位体前屈':72,'一分钟跳绳':80,'仰卧起坐':62,'折返跑':40}
print(res["levels"]) # {'肺活量':'不及格','50米跑':'及格',...}
print(res["bonus"], res["standard"], res["total"], res["level"]) # 0 59.4 59.4 '不及格'
批量处理(如大表循环):
from score_engine_2014 import score_one
def grade_student(row, grade="六年级"):
return score_one(grade, row["sex"], items={
"身高": row["height"], "体重": row["weight"],
"肺活量": row["lung"], "50米跑": row["run50"], "坐位体前屈": row["flex"],
"一分钟跳绳": row["rope"], "仰卧起坐": row["situp"], "折返跑": row["shuttle"],
})
初中 / 高中(男 1000米+引体向上,女 800米+仰卧起坐,引擎自动按性别取字段):
# 初一男生
res = score_one("初一", 1, items={
"身高": 172, "体重": 65, "肺活量": 4589, "50米跑": 9.12,
"立定跳远": 223, "坐位体前屈": 11, "1000米": 3.31, "引体向上": 4,
})
# 初一女生
res = score_one("初一", 2, items={
"身高": 164, "体重": 61, "肺活量": 3214, "50米跑": 10.34,
"立定跳远": 136, "坐位体前屈": 14, "800米": 3.5, "一分钟仰卧起坐": 16,
})
# res["bonus_detail"] 会给出各附加分:{'仰卧引体': 0, '耐力跑': 0}
> 耐力跑用"伪十进制":3.31 = 3分31秒、4.05 = 4分05秒;引体向上/仰卧起坐填个数。
字段说明
score_one(年级, 性别, heights=None, items=None) 返回 dict:
| 字段 | 说明 |
|---|---|
bmi |
BMI 值(保留3位小数,身高/体重缺失为 None) |
bmi_label |
正常 / 低体重 / 超重 / 肥胖 |
bmi_score |
BMI 得分:正常100,低体重80,超重80,肥胖60 |
scores |
各项目得分 dict(含 BMI),缺考项为 None |
levels |
各项目等级 dict(优秀/良好/及格/不及格/缺考) |
bonus |
附加分合计(跳绳/仰卧引体/耐力跑等,按年级规则) |
bonus_detail |
各项附加分明细,如 {'仰卧引体': 0, '耐力跑': 6.0};小学为 {'一分钟跳绳': n} |
standard |
标准分 = Σ(单项得分×权重) ÷ Σ(有成绩项权重)(已测项加权平均分,满分100);任何项目都无成绩则为 None |
total |
总分 = 标准分 + 附加分 |
level |
总等级:≥90优秀,≥80良好,≥60及格,<60不及格 |
参数
grade:'一年级'~'六年级'、'初一'~'初三'、'高一'~'高三'(与源表一致,用中文年级名)。sex:1=男,2=女;兼容'男'/'女'。items:dict,键取项目中文名。身高/体重也可改用heights=(cm, kg)单独传。- 各项目键名取项目中文名:
身高、体重、肺活量、50米跑、坐位体前屈、一分钟跳绳、仰卧起坐、折返跑(小学);初高中为身高、体重、肺活量、50米跑、立定跳远、坐位体前屈、1000米、800米、引体向上、一分钟仰卧起坐。传入该年级不考的项目会被自动忽略。
各年级项目与权重对照
| 年级 | 项目(含权重) |
|---|---|
| 一、二年级 | BMI 15% · 肺活量 15% · 50米跑 20% · 坐位体前屈 30% · 一分钟跳绳 20% |
| 三、四年级 | BMI 15% · 肺活量 15% · 50米跑 20% · 坐位体前屈 20% · 一分钟跳绳 20% · 仰卧起坐 10% |
| 五、六年级 | BMI 15% · 肺活量 15% · 50米跑 20% · 坐位体前屈 10% · 一分钟跳绳 10% · 仰卧起坐 20% · 50米×8折返跑 10% |
| 初、高中 | BMI 15% · 肺活量 15% · 50米跑 20% · 立定跳远 10% · 坐位体前屈 10% · 耐力跑 20% · 仰卧引体 10% |
> 一、二年级无仰卧起坐/折返跑;三、四年级无折返跑;初高中无跳绳/折返跑,改为耐力跑 + 仰卧引体。
> 传入多余项目会被忽略;缺项不参与计分——「有多少项评多少项」,标准分按实际有成绩项目的权重归一化到 100 分制。
>
> ⚠️ 注意三、四年级的仰卧起坐是 10%(不是 20%),这是最容易记错的一处权重。
关键语义(务必理解,避免误用)
- 时间类项目(50米跑、50×8折返跑、耐力跑)越短越好:阈值表按"时间升序 + 得分降序"排,VLOOKUP 近似匹配保证"达到某档位即取该档"(如 50米跑 10.09 秒落在 10.0=64分 与 10.2=62分 之间→取64分)。
- 折返跑原始值用"伪十进制"编码:
1.54= 1分54秒,2.10= 2分10秒。直接用数值代入比较,不要再转成秒。这与阈值表同一编码。 - 0 与文本标记的处理(修正后的口径,统一执行):
- 空单元格 / None → 该项缺考(得分 None),不参与标准分计算,其余项目照常——标准分按已测项权重归一化;
- 非数字文本标记(如"未测""免测""缓测""缺考")→ 视为缺考 → 该项 None、不参与计分,其余项目照常;
- 值为 0 一律按 0 分处理,不判缺考(即该项得 0 分、等级"不及格",但不使标准分/总分置空,其余项目照常计算):- 时间类项目(50米跑 / 折返跑 / 耐力跑)0 → 0 分。0 秒不可能,只可能是未测/漏录;引擎已做 0 值保护,不会误判为满分。
- 肺活量 / 仰卧起坐 / 跳绳 / 立定跳远 / 引体向上 = 0 → 该项 0 分,其余照常。
这正是"肺活量为 0 不得分,但不耽误其他项目得分"的规则。 - 坐位体前屈 = 0 → 是合法成绩(约 60 分),不是缺考。
仅当该生其余全部项目也都为空时,才说明整行没录,应判缺考——见下方judge_zero_value()。 - 字符串数字(如
"154""10.02")会被自动转为数值后正常评分。
- 跳绳附加分:一分钟跳绳超过"100分对应次数"后,每多跳2次 +1分,封顶 +20。100分基准:一109/117,二117/127,三126/139,四137/149,五148/158,六157/166(男/女)。
- 标准分按「有多少项评多少项」归一化:项目无成绩(该年级国标不测 / 学校未录 / 该生缺考)自动不参与,标准分 = Σ(得分×权重) ÷ Σ(有成绩项权重),即已测项目的加权平均分(100 分制);任何项目都无成绩才不出分。全项齐时与加权直接求和完全一致。
判缺考规则速查(引擎内置 _flag(raw, item))
None / 空串 → 该项缺考,不参与计分(标准分按已测项归一化)
非数字文本(未测/免测/缓测/缺考) → 该项缺考,不参与计分(标准分按已测项归一化)
任意项目 == 0 → 该项得 0 分,其余项照常,标准分/总分正常计算
字符串数字("154"/"10.02") → 转数值后正常评分
0 值语义判定:judge_zero_value()
「值为 0」到底是真实成绩还是没测,需要按项目分别判断。
判定规则与录入端、Excel 导入清洗共用同一份定义,避免"页面拦住了、导入没拦住"。
from score_engine_2014 import judge_zero_value
items = {"50米跑": 8.5, "坐位体前屈": 0, "一分钟跳绳": 120,
"仰卧起坐": 30, "折返跑": 2.05}
judge_zero_value(items, "50米跑", "六年级") # '未测' ← 0 秒不可能
judge_zero_value(items, "坐位体前屈", "六年级") # '真实' ← 0cm 是合法成绩(约60分)
judge_zero_value(items, "一分钟跳绳", "六年级") # '真实' ← 0 个是真实成绩
# 体前屈=0 且其余项目全空 → 说明整行没录
judge_zero_value({"50米跑": None, "坐位体前屈": 0, "一分钟跳绳": None},
"坐位体前屈", "六年级") # '未测'
| 项目类型 | 0 值判定 | 理由 |
|---|---|---|
| 时间类(50米跑 / 折返跑 / 耐力跑) | 未测 | 0 秒/0 分不可能 |
| 坐位体前屈 | 其余项全空 → 未测;否则 真实 | 0cm 是合法成绩(约 60 分) |
| 其他非时间类(肺活量 / 跳绳 / 仰卧起坐 / 立定跳远 / 引体向上) | 真实 | 0 是合法成绩 |
标准数据说明
standard2014_data.json 收录了《国家学生体质健康标准(2014 年修订)》的全套评分阈值,
覆盖小学一~六年级、初中初一~初三、高中高一~高三共 12 个年级,各项目均含男/女两套阈值,
并额外内置了两处一致性校准(四年级跳绳女、初三体前屈男),确保与《国标》档位间隔规律一致。
> 建议把「validate_standard() 校验归零」作为更新评分标准前的准入门槛,避免异常阈值进入数据。
阈值表校验(每次更新标准后必跑)
from score_engine_2014 import validate_standard
issues = validate_standard() # 返回问题清单,空 list = 全部通过
for i in issues:
print(i["年级"], i["项目"], i["性别"], i["类型"], i["档位"], i["详情"])
当前 standard2014_data.json:0 处问题。
校四项:
| 检查项 | 说明 |
|---|---|
| 阈值严格升序 | VLOOKUP 近似匹配的前提,乱序会查出错分 |
| 阈值不重复 | 重复档位命中哪个不确定 |
| 得分单调 | 高优类随阈值升而升;时间类随阈值升而降 |
| 时间类首档有 0 值保护 | 首档必须是 [0,0],否则成绩填 0 会得满分或 #N/A |
> skip_sentinel=True(默认)会跳过 0 值保护档的单调性检查——
> 保护档 [0,0] 天然破坏得分单调(前 0 分、后 100 分),属预期,不算问题。
建议把「校验归零」作为更新评分标准的准入门槛。
验证(引擎与 Excel 逐人一致)
引擎已与按《国标》编制的 Excel 算分表在小学 / 初中 / 高中多轮全量对拍:全项齐的行标准分、总分、等级逐人一致(含 0 值、人工置分的归类逻辑一致);缺项行引擎按已测项归一化出分(Excel 源表为置空),这是 2026-10-13 起的有意变更。
0 值处理实测行为
| 1000 米成绩 | 耐力跑得分 |
|---|---|
| 3.11(满分线内) | 100 |
| 3.40 | 90 |
| 0(未测/漏录) | 0 |
| 空 | 空(缺考) |
迁移给第三方使用
本目录是一个自包含 skill,迁移只需把整个目录拷贝过去,无需安装任何第三方依赖(引擎仅标准库)。
方式 A:作为 WorkBuddy skill 迁移
把整个 tizhi-score-2014 目录放到目标机器的 skills 目录:
- Windows 用户级:
C:\Users\<用户名>\.workbuddy\skills\tizhi-score-2014\ - 项目级共享:
<项目>\.workbuddy\skills\tizhi-score-2014\
放入后即被 WorkBuddy 识别为可用 skill;对话中出现「体测/算分/达标/跳绳/仰卧起坐/50米跑/坐位体前屈/折返跑/BMI」等即可触发。
方式 B:作为纯 Python 包迁移(给第三方开发者 / 其他系统)
只把两个核心文件拷过去,放同一目录:
score_engine_2014.pystandard2014_data.json
任何 Python 环境:
import sys
sys.path.insert(0, r"这里填引擎所在目录")
from score_engine_2014 import score_one
res = score_one("六年级", 1, items={...})
引擎用 os.path.dirname(__file__) 自动定位 json,不依赖当前工作目录,拷到哪都能用。
依赖说明
- 引擎本身:仅标准库(
json/os/bisect/math/decimal),零第三方依赖。
扩展新年级(引擎零改动,只补数据)
引擎是纯数据驱动的——项目集合、权重、阈值表、时间类项目、附加分项全部从 standard2014_data.json 读取。
因此新增年级不需要改一行 Python,只要在 json 里按同样结构加上年级键:
"某某年级": {
"weights": {"BMI": 0.15, "肺活量": 0.15, "50米跑": 0.2, "坐位体前屈": 0.1,
"立定跳远": 0.1, "仰卧引体": 0.1, "耐力跑": 0.2},
"BMI_bounds": {"male": [低体重上界, 正常上界, 超重下界], "female": [...]},
"肺活量": {"male": [[阈值,得分], ...], "female": [...]},
"50米跑": {"male": [[阈值,得分], ...], "female": [...]},
"坐位体前屈": {...}, "立定跳远": {...},
"耐力跑": {"male": [[阈值,得分], ...], "female": [...], "male_source": "1000米", "female_source": "800米"},
"仰卧引体": {"male": [[阈值,得分], ...], "female": [...], "male_source": "引体向上", "female_source": "一分钟仰卧起坐"},
"round": {"50米跑": 1, "耐力跑": 1, "坐位体前屈": 1},
"time_items": ["50米跑", "耐力跑"],
"bonus_items": ["仰卧引体", "耐力跑"],
"bonus_rules": {...}
}
要点:
- weights 的键顺序 = 项目集合,权重和必须 = 1.0;
- 每张阈值表 [[阈值, 得分], ...] 阈值必须升序(时间类项目得分自然降序);
- 时间类项目(越短越好、0 得 0 分)写进该年级的 time_items;
- 男女生考不同项目时(如耐力跑男 1000米 / 女 800米),用 male_source / female_source 指定原始字段名;
- 前置处理:源表若对某些列先做 ROUND(x,n) 或 INT(x) 再查表,务必在 round 里声明,否则会有零点几分误差;
- 附加分:bonus_rules 支持 rope_step(跳绳,每 step 次 +1 分,封顶 cap)/ step(计次类,如仰卧引体)/ time_gap(时间类,按秒差查表)。
> 若你已有 Excel 算分表,可解析其名称管理器(xl/workbook.xml 的 <definedNames>)里 VLOOKUP 公式中的阈值数组,自动生成上述 json 结构——比手抄更可靠。
files
score_engine_2014.py—— 评分引擎(import 即用)score_one()单人评分(缺项归一化:有多少项评多少项)list_grades()/grade_groups()/describe_grade()辅助查询validate_standard()阈值表全量校验(每次更新标准后必跑)judge_zero_value()0 值语义判定(未测 vs 真实成绩)standard2014_data.json—— 2014 标准数据(12 年级齐全;40 张时间类表已内置 0 值保护档;2 处阈值已校准)README.md—— 概览与一致性证据安装与使用说明.md—— 面向使用者的安装步骤与可复制提示词
更新历史
| 日期 | 内容 || 2026-10-13 | 提升 skillhub 可发现性:displayName 改为「学生体测评分计算器(国标2014·12年级全覆盖)」、summary/description 改为以「体测评分/算分/达标统计」等用户真实检索词开头(原为「按《…》计算」,'按'字无语义会稀释核心词)、新增 tags;slug 保持 tizhi-score-2014 不变(沿用已发布版本历史) |
|---|---|
| 2026-09-28 | 时间类阈值表加 0 值保护档;校准 2 处标准阈值(四年级跳绳女、初三体前屈男);新增 validate_standard() / judge_zero_value();统一 0 值语义(0 一律得 0 分,不判缺考) |
| 2026-09-30 | 发布版精简:仅保留全量计算(full)版本;移除缺项折算(partial_scoring / normalized / history 多口径)及 Excel 专用对拍脚本、分析模板,便于对外发布 |
| 2026-09-30 | 对外发布版清理:移除内部修复脚本与阈值校准过程叙述,仅保留最终功能效果;去除本地文件名与具体班级引用 |
| 2026-10-11 | 修复耐力跑附加分表(time_gap):高一/高二/高三男生 1000 米及全学段女生 800 米的高分档秒差被整体多加 40 秒,导致长跑领先者附加分漏算;按官方加分表修正为男 1000 米 4/8/…/35 秒、女 800 米 5/10/…/50 秒(共 9 张表) |
| 2026-10-13 | 评分口径改为「有多少项评多少项」:项目无成绩(该年级国标不测 / 学校未录 / 该生缺考)自动不参与计分,标准分按实际有成绩项目的权重归一化到 100 分制,任何项目都无成绩才不出分;全项齐时与原算法完全一致 |