581 lines
20 KiB
Python
581 lines
20 KiB
Python
#!/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, '男', {...})))\"")
|