Files

581 lines
20 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""
国民体质测定标准 — 评测计算引擎
输入:年龄、性别、各项测试指标原始值
输出:单项得分、综合加权得分、综合评级
"""
import json
import math
import sys
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_PATH = os.path.join(BASE_DIR, '评分标准结构化数据.json')
def load_scoring_data(path=DATA_PATH):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def determine_section(age):
"""根据年龄确定所属部分"""
if 3 <= age <= 6:
return '幼儿'
elif 20 <= age <= 59:
return '成年人'
elif 60 <= age <= 79:
return '老年人'
else:
raise ValueError(f"年龄 {age} 不在标准覆盖范围(3-79岁)")
def determine_age_group(age, section):
"""确定年龄组名称(与JSON中一致)"""
if section == '幼儿':
# 3-5岁每0.5岁一组,6岁单独一组
if 3 <= age < 6:
# 取最近的半岁
base = math.floor(age)
remainder = age - base
if remainder < 0.25:
return f"{base}岁"
elif remainder < 0.75:
return f"{base}.5岁"
else:
return f"{base + 1}岁"
else: # age == 6
return "6岁"
elif section == '成年人':
# 每5岁一组
low = (age // 5) * 5
high = low + 4
return f"{low}-{high} 岁"
elif section == '老年人':
# 每5岁一组
low = (age // 5) * 5
high = low + 4
return f"{low}-{high} 岁"
def find_score_in_standard_table(score_bands, age_group, value):
"""
在标准评分表中查找得分
score_bands: [{"score": 10, "ranges": {"3岁": {"max": 92.1}, ...}}, ...]
规则:从高到低匹配,第一个匹配的分数
"""
# 从高到低排序
sorted_bands = sorted(score_bands, key=lambda x: x['score'], reverse=True)
for band in sorted_bands:
score = band['score']
ranges = band['ranges']
if age_group not in ranges:
continue
r = ranges[age_group]
if 'min' in r and 'max' in r:
if r['min'] <= value <= r['max']:
return score
elif 'min' in r and value >= r['min']:
return score
elif 'max' in r and value <= r['max']:
return score
elif 'exact' in r and abs(value - r['exact']) < 0.001:
return score
return 0 # 超出所有范围
def find_score_in_bmi_table(bmi_data, age_group, value):
"""
在BMI/体脂率分段评分表中查找得分
bmi_data.data: {"36月": [{"score": 60, "max": 13.4}, {"score": 100, "min": 13.4, "max": 18.4}, ...], ...}
这些表是"banded"类型,按行=年龄,列=分数档
"""
age_records = bmi_data.get(age_group, [])
if not age_records:
# 对于成年人BMI,年龄组可能是"20-59 岁"(只有一个组)
# 尝试找第一个key(可能是唯一组)
if bmi_data:
first_key = list(bmi_data.keys())[0]
age_records = bmi_data.get(first_key, [])
if not age_records:
return 0
for record in age_records:
score = record['score']
r = record # 直接包含 min/max
if 'min' in r and 'max' in r:
if r['min'] <= value <= r['max']:
return score
elif 'min' in r and value >= r['min']:
return score
elif 'max' in r and value <= r['max']:
return score
return 0
def get_age_group_key_for_section(age, section, lookup):
"""获取JSON中该年龄对应的年龄组key"""
if section == '幼儿':
# 先尝试半岁组
age_group = determine_age_group(age, section)
return age_group
elif section == '成年人':
# 成人5岁一组
return determine_age_group(age, section)
else:
return determine_age_group(age, section)
def get_section_for_age_key(age_group, section_data):
"""针对幼儿BMI表需要用月度key"""
return age_group # 默认一致
def score_indicator(section, gender, indicator, value, age, sl):
"""
对单个指标评分
返回: (score, indicator_name)
"""
if section == '幼儿' and gender == '男':
gender_key = 'male'
elif section == '幼儿' and gender == '女':
gender_key = 'female'
elif section == '成年人' and gender == '男':
gender_key = 'male'
elif section == '成年人' and gender == '女':
gender_key = 'female'
elif section == '老年人' and gender == '男':
gender_key = 'male'
elif section == '老年人' and gender == '女':
gender_key = 'female'
else:
gender_key = 'male'
# 通用指标(BMI等性别中立的)
table = None
if indicator in sl[section].get('通用', {}):
table = sl[section]['通用'][indicator]
elif indicator in sl[section].get(gender_key, {}):
table = sl[section][gender_key][indicator]
else:
return (0, f"未找到 {section}/{gender}/{indicator} 的评分表")
tbl_type = table['type']
age_group = determine_age_group(age, section)
if tbl_type == 'standard_scoring':
age_groups = table['data']['age_groups']
# 对幼儿处理BMI的月份key(如果age_group不是月格式)
if section == '幼儿' and indicator == 'BMI':
# 幼儿BMI用月龄key
month_key = f"{int(age * 12)}月"
if month_key in age_groups:
age_group = month_key
# 匹配年龄组
if age_group not in age_groups:
age_group = normalize_age_group(age_group, age_groups)
score = find_score_in_standard_table(
table['data']['score_bands'],
age_group,
value
)
return (score, indicator)
elif tbl_type == 'bmi_banded':
data = table['data']['data']
age_groups = list(data.keys())
if section == '幼儿':
# 幼儿BMI用月龄
month_key = f"{int(age * 12)}月"
if month_key in age_groups:
age_group = month_key
score = find_score_in_bmi_table(data, age_group, value)
return (score, indicator)
return (0, indicator)
def normalize_age_group(age_group, available_groups):
"""匹配年龄组名,处理空格/格式差异"""
# 精确匹配
if age_group in available_groups:
return age_group
# 去空格匹配
age_stripped = age_group.replace(' ', '')
for ag in available_groups:
if ag.replace(' ', '') == age_stripped:
return ag
# 前缀匹配
for ag in available_groups:
if ag.startswith(age_group[:4]) or age_group.startswith(ag[:4]):
return ag
return age_group # 找不到则原样返回
def compute_bmi(height_cm, weight_kg):
"""计算BMI"""
height_m = height_cm / 100
return weight_kg / (height_m ** 2)
def evaluate_person(age, gender, test_values, scoring_data=None):
"""
评测一个人
参数:
age: 年龄(周岁)
gender: '男' 或 '女'
test_values: dict,指标名→测试值
- 幼儿: 身高(cm), 体重(kg), 握力(kg), 立定跳远(cm), 坐位体前屈(cm),
双脚连续跳(秒), 15米绕障碍跑(秒), 走平衡木(秒)
- 成年人: 身高(cm), 体重(kg), 体脂率(%), 肺活量(ml), 功率车(ml/kg/min),
握力(kg), 纵跳(cm), 俯卧撑(次)/跪卧撑(次), 仰卧起坐(次/分),
坐位体前屈(cm), 闭眼单脚站立(秒), 选择反应时(秒)
- 老年人: 身高(cm), 体重(kg), 体脂率(%), 肺活量(ml), 2分钟高抬腿(次),
握力(kg), 坐位体前屈(cm), 30秒坐站(次), 闭眼单脚站立(秒),
选择反应时(秒)
返回: dict
"""
if scoring_data is None:
scoring_data = load_scoring_data()
sl = scoring_data['scoring_lookup']
section = determine_section(age)
# 确定年龄段标签
age_group = determine_age_group(age, section)
results = {
'age': age,
'gender': gender,
'section': section,
'age_group': age_group,
'indicators': {},
'total_score': 0,
'rating': '',
'errors': []
}
# 1. 先计算BMI(如果有身高体重)
bmi_value = None
height_cm = test_values.get('身高(cm)', test_values.get('身高'))
weight_kg = test_values.get('体重(kg)', test_values.get('体重'))
if height_cm and weight_kg:
bmi_value = compute_bmi(height_cm, weight_kg)
test_values['BMI'] = round(bmi_value, 1)
# 2. 对各指标评分
# 标准化指标名映射(去除单位后缀)
indicator_alias = {
'体脂率(%)': '体脂率', '体脂率': '体脂率',
'肺活量(ml)': '肺活量', '肺活量': '肺活量',
'功率车(ml/kg/min)': '功率车', '功率车': '功率车',
'握力(kg)': '握力', '握力': '握力',
'纵跳(cm)': '纵跳', '纵跳': '纵跳',
'俯卧撑(次)': '俯卧撑', '俯卧撑': '俯卧撑',
'跪卧撑(次)': '跪卧撑', '跪卧撑': '跪卧撑',
'仰卧起坐(次/分)': '仰卧起坐', '仰卧起坐': '仰卧起坐',
'坐位体前屈(cm)': '坐位体前屈', '坐位体前屈': '坐位体前屈',
'立定跳远(cm)': '立定跳远', '立定跳远': '立定跳远',
'双脚连续跳(秒)': '双脚连续跳', '双脚连续跳': '双脚连续跳',
'15米绕障碍跑(秒)': '绕障碍跑', '15米绕障碍跑': '绕障碍跑',
'走平衡木(秒)': '走平衡木', '走平衡木': '走平衡木',
'闭眼单脚站立(秒)': '闭眼单脚站立', '闭眼单脚站立': '闭眼单脚站立',
'选择反应时(秒)': '选择反应时', '选择反应时': '选择反应时',
'2分钟高抬腿(次)': '高抬腿', '高抬腿': '高抬腿',
'30秒坐站(次)': '坐站', '坐站': '坐站',
'身高(cm)': '身高', '身高': '身高',
'体重(kg)': '体重', '体重': '体重',
}
for raw_indicator, value in test_values.items():
if value is None or value == '':
continue
try:
value = float(value)
except (ValueError, TypeError):
continue
# 标准化指标名
indicator = indicator_alias.get(raw_indicator, raw_indicator)
# 体重不直接评分(通过BMI反映)
if indicator == '体重':
continue
# 身高在成年人/老年人不单独评分(通过BMI反映)
if section != '幼儿' and indicator == '身高':
continue
score, matched_name = score_indicator(section, gender, indicator, value, age, sl)
results['indicators'][indicator] = {
'value': value,
'score': score,
'matched_indicator': matched_name,
'input_key': raw_indicator
}
# 3. 计算加权总分
total_weighted = 0.0
total_weight_used = 0.0
# 确定使用的权重表
if section == '幼儿':
weight_table = sl['weights'].get('幼儿', {})
elif section == '成年人':
if age <= 49:
weight_table = sl['weights'].get('成年人_20-49岁', sl['weights'].get('成年人', {}))
else:
weight_table = sl['weights'].get('成年人_50-59岁', sl['weights'].get('成年人', {}))
elif section == '老年人':
weight_table = sl['weights'].get('老年人_60-79岁', sl['weights'].get('老年人', {}))
else:
weight_table = {}
# 权重表里的指标名可能与评分时的指标名不同,需要映射
weight_to_indicator = {
'体重指数(BMI)': 'BMI',
'身高': '身高',
'体脂率': '体脂率',
'肺活量': '肺活量',
'功率车二级负荷试验': '功率车',
'握力': '握力',
'纵跳': '纵跳',
'俯卧撑(男)/跪卧撑(女)': '俯卧撑',
'1分钟仰卧起坐': '仰卧起坐',
'坐位体前屈': '坐位体前屈',
'立定跳远': '立定跳远',
'双脚连续跳': '双脚连续跳',
'15 米绕障碍跑': '绕障碍跑',
'走平衡木': '走平衡木',
'闭眼单脚站立': '闭眼单脚站立',
'选择反应时': '选择反应时',
'2分钟原地高抬腿': '高抬腿',
'30秒坐站': '坐站',
}
# 反向映射:评分指标名 → 权重表指标名
rev_map = {v: k for k, v in weight_to_indicator.items()}
scoring_details = []
for wt_name, weight in weight_table.items():
indicator_key = weight_to_indicator.get(wt_name, wt_name)
scored = results['indicators'].get(indicator_key)
# 如果直接映射没找到,尝试模糊匹配:权重键包含评分指标名
if scored is None:
for scored_key in results['indicators']:
if scored_key in wt_name or wt_name in scored_key:
scored = results['indicators'][scored_key]
indicator_key = scored_key
break
if scored is None:
# 尝试直接匹配
scored = results['indicators'].get(wt_name)
if scored:
pts = scored['score'] * weight
total_weighted += pts
total_weight_used += weight
scoring_details.append({
'indicator': indicator_key,
'weight': weight,
'score': scored['score'],
'weighted_pts': round(pts, 2)
})
# 处理剩余未在权重表里但已评分的指标
for indicator, scored in results['indicators'].items():
if indicator not in [s['indicator'] for s in scoring_details]:
scoring_details.append({
'indicator': indicator,
'weight': 0,
'score': scored['score'],
'weighted_pts': 0,
'note': '未在权重表中'
})
results['scoring_detail'] = scoring_details
results['total_score'] = round(total_weighted, 1)
results['weight_coverage'] = round(total_weight_used, 2)
# 4. 确定综合评级
rating_levels = sl['rating_levels'].get(section, {})
rating = '未知'
for level_name, criteria in rating_levels.items():
if 'min' in criteria and 'max' in criteria:
if criteria['min'] <= total_weighted < criteria['max']:
rating = level_name
break
elif 'min' in criteria and total_weighted >= criteria['min']:
rating = level_name
break
elif 'max' in criteria and total_weighted < criteria['max']:
rating = level_name
break
results['rating'] = rating
return results
def format_report(result):
"""格式化输出评测报告"""
lines = []
lines.append("=" * 60)
lines.append(" 国民体质测定标准 — 评测报告")
lines.append("=" * 60)
lines.append(f" 年龄:{result['age']}岁 性别:{result['gender']}")
lines.append(f" 年龄段:{result['section']}({result['age_group']}组)")
lines.append("-" * 60)
lines.append(f" {'指标':<16} {'实测值':<10} {'得分':<6} {'权重':<6} {'加权分':<8}")
lines.append("-" * 60)
for item in result.get('scoring_detail', []):
indicator = item['indicator']
# 找实测值
value = result['indicators'].get(indicator, {}).get('value', '')
if isinstance(value, (int, float)):
value_str = f"{value:.1f}"
else:
value_str = str(value)
note = item.get('note', '')
weight_str = f"{item['weight']:.2f}" if item['weight'] > 0 else '-'
pts_str = f"{item['weighted_pts']:.1f}" if item['weighted_pts'] > 0 else '-'
indicator_show = indicator[:14]
lines.append(f" {indicator_show:<16} {value_str:<10} {item['score']:<6} {weight_str:<6} {pts_str:<8}")
lines.append("-" * 60)
lines.append(f" 综合得分:{result['total_score']:.1f}")
lines.append(f" 权重覆盖率:{result['weight_coverage']:.0%}")
lines.append(f" 综合评级:{result['rating']}")
if result.get('errors'):
lines.append("-" * 60)
for err in result['errors']:
lines.append(f" ⚠ {err}")
lines.append("=" * 60)
return '\n'.join(lines)
def interactive_eval():
"""交互式评测"""
print("国民体质测定标准 — 交互式评测")
print("=" * 50)
try:
age = int(input("年龄(周岁): "))
gender = input("性别(男/女): ").strip()
if gender not in ['男', '女']:
print("性别输入错误")
return
except ValueError:
print("年龄输入错误")
return
section = determine_section(age)
print(f"\n→ 适用部分:{section}({determine_age_group(age, section)}组)")
# 提示应输入哪些指标
if section == '幼儿':
prompts = ['身高(cm)', '体重(kg)', '握力(kg)', '立定跳远(cm)',
'坐位体前屈(cm)', '双脚连续跳(秒)', '15米绕障碍跑(秒)', '走平衡木(秒)']
elif section == '成年人':
prompts = ['身高(cm)', '体重(kg)', '体脂率(%)', '肺活量(ml)',
'功率车(ml/kg/min)', '握力(kg)', '纵跳(cm)',
'俯卧撑(次)', '跪卧撑(次)', '仰卧起坐(次/分)',
'坐位体前屈(cm)', '闭眼单脚站立(秒)', '选择反应时(秒)']
else:
prompts = ['身高(cm)', '体重(kg)', '体脂率(%)', '肺活量(ml)',
'2分钟高抬腿(次)', '握力(kg)', '坐位体前屈(cm)',
'30秒坐站(次)', '闭眼单脚站立(秒)', '选择反应时(秒)']
test_values = {}
print(f"\n请输入测试值(直接回车跳过可选指标):")
for prompt in prompts:
val = input(f" {prompt}: ").strip()
if val:
try:
test_values[prompt] = float(val)
except ValueError:
test_values[prompt] = val
result = evaluate_person(age, gender, test_values)
print()
print(format_report(result))
return result
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] in ('--demo', '-d'):
# 演示模式
data = load_scoring_data()
print("=" * 60)
print(" 演示1:成年男性,35岁")
print("=" * 60)
result = evaluate_person(35, '男', {
'身高(cm)': 175, '体重(kg)': 78, '体脂率(%)': 22,
'肺活量(ml)': 3500, '功率车': 38,
'握力(kg)': 42, '纵跳(cm)': 32,
'俯卧撑(次)': 25, '仰卧起坐(次/分)': 20,
'坐位体前屈(cm)': 8, '闭眼单脚站立(秒)': 35,
'选择反应时(秒)': 0.42
})
print(format_report(result))
print()
print("=" * 60)
print(" 演示2:幼儿女性,5岁")
print("=" * 60)
result2 = evaluate_person(5, '女', {
'身高(cm)': 110, '体重(kg)': 18,
'握力(kg)': 5, '立定跳远(cm)': 80,
'坐位体前屈(cm)': 6, '双脚连续跳(秒)': 7.5,
'15米绕障碍跑(秒)': 10.5, '走平衡木(秒)': 12
})
print(format_report(result2))
print()
print("=" * 60)
print(" 演示3:老年男性,68岁")
print("=" * 60)
result3 = evaluate_person(68, '男', {
'身高(cm)': 168, '体重(kg)': 70, '体脂率(%)': 24,
'肺活量(ml)': 2500, '2分钟高抬腿(次)': 80,
'握力(kg)': 32, '坐位体前屈(cm)': -2,
'30秒坐站(次)': 15, '闭眼单脚站立(秒)': 12,
'选择反应时(秒)': 0.65
})
print(format_report(result3))
elif len(sys.argv) > 1 and sys.argv[1] in ('--interactive', '-i'):
interactive_eval()
else:
print("用法:")
print(" python3 fitness_eval.py -d # 运行演示模式")
print(" python3 fitness_eval.py -i # 交互式评测")
print(" python3 -c \"import fitness_eval; print(fitness_eval.format_report(fitness_eval.evaluate_person(35, '男', {...})))\"")