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