简介:一份基于Python实现的康复评估系统源码与配套数据集,面向计算机、人工智能、通信工程、自动化等专业的在校生和开发者,可用于毕业设计、课程设计、项目初期立项及演示。系统聚焦人体动作数据采集与分析,利用bvh动作捕捉数据和csv结构化记录,可在Python环境下完成动作读取、特征提取与康复评估逻辑搭建,便于理解完整处理流程并支撑后续功能迭代。整个资源共157个文件,除了11个py源码和2个ipynb笔记本外,还有68个bvh动作样本、70个csv数据表及md说明文档等,包体仅16.18MB,结构紧凑,便于快速下载和本地运行。此项目代码经过功能测试,可直接导入IDE运行,也适合在此基础上扩展其他康复指标、图表展示或交互界面。目前已吸引260人学习浏览,对需要搭建可演示系统或理解动作数据处理的读者来说,是一份省时高效的参考实现。
1. 从题目到交付物:这套康复评估系统到底解决了什么
康复评估系统听起来像是个医疗级产品,但做过毕业设计或课程设计的人都知道,真正难的不是评估算法本身,而是数据从哪来、评估标准怎么落地成代码。这套基于 Python 实现的康复评估系统源码,配合自带数据集,正好把医院康复科日常的评估工作——关节活动度测量、肌力分级、日常生活能力评分——拆成了可运行的代码和可训练的数据。拿到手你能直接看到完整的项目结构,从数据读取、清洗到指标计算和结果输出,是一条走得通的链路,不是只有几个孤立脚本。适合两类人:一类是需要毕业设计选题,想找一个有数据、有逻辑、能演示的完整项目的学生;另一类是接了康复相关课程设计、想快速搭出原型并跑通数据的开发者。接下来我会从工程角度拆解这套系统的模块、数据坑和算法实现,重点讲清楚每个环节怎么落地。
2. 先看懂系统骨架:模块划分、数据字典与评估流程
拿到源码之后,第一件事不是跑起来,而是把项目结构读明白。康复评估系统最常见的工程划分是数据层、评估层和展示层三层。数据层负责读取患者记录、量表和传感器数据;评估层把原始数据换算成评估指标;展示层把结果输出成报告或图表。这套源码基本也是按这个思路组织的,理解了这个骨架,后面改参数、加功能才找得到地方。
2.1 模块划分:从源码目录反推设计思路
我拆这类项目有个习惯,先看目录不看代码。一个规范的 Python 项目,目录能直接告诉你作者的设计逻辑。这套系统的核心模块通常包含数据读取模块、评估计算模块、结果输出模块和工具函数模块。数据读取模块处理 CSV、Excel 这类表格格式,把患者基本信息、量表评分、关节角度原始记录统一加载成 DataFrame;评估计算模块是核心,封装了关节活动度(ROM)计算、肌力分级判定和 Barthel 指数评分等逻辑;结果输出模块负责生成可视化图表和评估报告。
代码组织上,各模块保持相对独立,函数粒度适中。比如 ROM 计算不会混在界面代码里,而是独立的类或函数集合。这样设计的好处很明显:毕设答辩时你可以单独演示每个函数输入输出,也能方便地替换算法实现。我在实际改造这类项目时,通常会把各个评估功能和数据处理功能拆成独立的包,结构类似:
rehab_assessment/ ├── data/ # 原始数据集与清洗脚本 │ ├── raw/ # 原始 CSV、Excel 数据 │ ├── processed/ # 清洗后的数据 │ └── process_data.py ├── modules/ # 核心评估算法 │ ├── rom.py # 关节活动度计算 │ ├── muscle_strength.py # 肌力分级 │ ├── barthel_index.py # Barthel 指数评估 │ └── balance.py # 平衡功能评估 ├── output/ # 评估报告与图表输出 ├── main.py # 主流程入口 └── requirements.txt这个排列方式和原始项目的设计意图是一致的:数据与逻辑分离,评估算法可独立调用。main.py 把整条流程串起来,读入数据、逐项评估、汇总输出。
模块划分里最容易踩的坑是数据读取和评估逻辑耦合。有些项目会把读取 Excel 的代码写在评估函数内部,导致换数据格式时逻辑也要跟着改。这套系统在这一点上做得比较干净,数据加载和指标计算是分开的,后续扩展成 Web 服务或桌面应用都留了余地。
2.2 数据字典:理解每一列到底是什么
康复评估系统的数据集不会像目标检测数据集那样是一堆图片加标注框,它更像医疗记录,是结构化的表格数据。你需要先搞清楚每个字段的含义,否则后面的计算全是空中楼阁。通常包括患者基础信息字段、评估项目得分字段和原始测量记录字段。
患者基础信息字段包括患者 ID、年龄、性别、诊断类型;评估项目字段包括关节名称、测量方向、健侧角度、患侧角度、肌力等级、Barthel 各项得分等。以关节活动度数据为例,一行记录至少要有患者 ID、关节名称(肩关节、膝关节等)、活动方向(屈曲、伸展、外展)、健侧角度和患侧角度。角度值一般用度表示,整型或保留一位小数。
Barthel 指数部分的数据则以评分项为列,常见的是十项评分:进食、洗澡、修饰、穿衣、控制大便、控制小便、如厕、床椅转移、平地行走、上下楼梯。每一项的得分不是连续的,而是阶梯式的,比如进食 0 分代表完全依赖,5 分代表需部分帮助,10 分代表独立。这种非连续评分在实际处理时有一个常见误区——直接把得分当连续数值参与均值计算,这在统计上是错的,因为分数间隔不代表等距能力差异。
import pandas as pd # 加载量表数据 df = pd.read_csv('data/raw/barthel_scores.csv', encoding='utf-8-sig') print(df.head()) print(df.dtypes) # 对评分列做统一处理:确保读取为数值类型 score_cols = ['进食', '洗澡', '修饰', '穿衣', '控制大便', '控制小便', '如厕', '床椅转移', '平地行走', '上下楼梯'] for col in score_cols: df[col] = pd.to_numeric(df[col], errors='coerce') # 计算总分 df['barthel_total'] = df[score_cols].sum(axis=1) print(df[['患者ID', 'barthel_total']].describe())这里用了utf-8-sig编码读取 CSV,原因是康复评估系统的数据大多来自国内医院或临床记录,Excel 另存的 CSV 经常带 BOM 头,不指定这个编码会导致第一列列名出现\ufeff前缀的乱码。pd.to_numeric加上errors='coerce'是为了把可能混入的文本格式数字强制转成数值,转换失败的值变成 NaN,方便后续统一处理缺失值。
字段类型确认后,建议先做一次全量统计:每列的最大值、最小值、缺失值数量。很多原始数据里,角度值和量表得分混着单位或备注文本,这些脏数据在计算前必须清掉,否则评估结果会带进明显错误。数据字典这一步花 10 分钟,后面能省两小时。
2.3 评估流程串联:一条数据从原始记录到评估报告
把模块和数据字典都搞清楚了,下一步就是看数据是怎么在系统里流动的。一次完整的评估流程,从输入原始记录开始,到输出评估报告结束,中间经过数据清洗、指标计算、分级判定三个阶段。
数据清洗阶段做的事情是把原始表格变成统一结构。原始数据里常出现的情况包括:同一患者多次测量但记录行格式不一致、某几项量表得分空着、角度值写成文本型。清洗的目标是让每条记录都能被评估函数正确读取。指标计算阶段是最核心的,ROM 模块算出各个关节的活动度值,肌力分级模块根据徒手肌力测试结果映射到 0-5 级,Barthel 模块按规则累加得分。分级判定阶段则是把连续分数变成康复诊断里常用的等级结论。
这套流程串联起来之后,你会发现康复评估系统的本质是一个规则驱动加阈值判定的系统,核心输出是结构化的评估结果,而不是像深度学习模型那样的概率输出。这样的系统在毕设展示时有一个明显优势:每个中间步骤的结果都能打印出来,答辩时可以从数据讲起,一步一步推导到最终结论。
from modules.rom import calculate_rom from modules.muscle_strength import classify_strength from modules.barthel_index import calculate_barthel # 读取一条患者记录 patient_record = { 'rom_data': {'肩关节屈曲': {'健侧': 165, '患侧': 92}}, 'muscle_test': {'肩外展肌力': 3}, 'daily_activity': {'进食': 10, '穿衣': 5, '平地行走': 10} } # 依次执行评估 rom_result = calculate_rom(patient_record['rom_data']) strength_result = classify_strength(patient_record['muscle_test']) barthel_result = calculate_barthel(patient_record['daily_activity']) # 汇总结果 assessment_summary = { 'rom': rom_result, 'strength': strength_result, 'barthel_total': barthel_result['total_score'], 'barthel_level': barthel_result['level'] } print(assessment_summary)这个例子展示了系统主流程的组织方式:每条评估逻辑都封装在独立模块里,主程序只负责编排顺序和汇总结果。实际项目中你可以在数据载入后用 DataFrame 批量处理所有患者,而不需要逐条构造字典。
3. 数据准备与预处理:原始量表记录如何变成可用的数据集
数据集质量直接决定评估系统的可信度。康复评估系统配套的数据集通常不会太干净,因为临床记录本身就有各种历史遗留问题。拿到数据集后,预处理环节必须手动过一遍,把明显错误、缺失、格式不统一的数据处理掉,再谈后续计算。
3.1 清洗逻辑:处理缺失值、异常值和文本污染
康复数据里最常出现的三类问题:空值、越界值、文本混入。空值比较好理解,某次评估没做或者没记录,单元格是空的。越界值是指角度超过正常生理范围,比如膝关节屈曲记录成 170 度,这明显是录入错误。文本混入则是数值列里出现了“左肩”“未测”这类说明文字。
清洗时要区分两类缺失:真的没测,和应该有值但没录进去。量表评估里,如果某项缺失,按康复评估的通行做法是该项计 0 分,表示完全依赖或无法完成;但关节角度缺失就不能直接补 0,否则计算出来的活动度会显示患者关节完全僵直,与实际不符。常见做法是先剔除角度缺失的整条记录,或者用该关节的同侧多次测量均值填充,但填充逻辑要在文档里说明。
import pandas as pd import numpy as np # 读取原始数据 df = pd.read_csv('data/raw/rom_records.csv', encoding='utf-8-sig') # 去掉角度值明显超出正常范围的记录(肩关节屈曲正常范围 0-180 度) df = df[(df['健侧角度'] >= 0) & (df['健侧角度'] <= 180)] df = df[(df['患侧角度'] >= 0) & (df['患侧角度'] <= 180)] # 处理文本污染:尝试转数值,失败则置空 for col in ['健侧角度', '患侧角度']: df[col] = pd.to_numeric(df[col], errors='coerce') # 删除关键字段为空的行 df_clean = df.dropna(subset=['患者ID', '关节名称', '健侧角度', '患侧角度']) # 用同一患者同一关节的多次测量均值填充剩余空值 df_clean['患侧角度'] = df_clean.groupby(['患者ID', '关节名称'])['患侧角度'].transform( lambda x: x.fillna(x.mean()) ) print(f"清洗前记录数: {len(df)},清洗后记录数: {len(df_clean)}")这段代码的关键是groupby + transform填充。先按患者 ID 和关节名称分组,组内均值填充缺失值,这样比全局均值更合理,因为不同患者的关节活动度基线差异很大,用整个数据集均值填充会掩盖个体差异。清洗完成后,对比清洗前后的记录数,能直观看到丢失了多少数据,这在你写毕设论文数据说明部分时是必需的。
角度过滤那里要注意生理范围阈值不是固定的,不同关节差异很大。肩关节屈曲可以到 180 度,但肘关节屈曲是 150 度左右,腕关节更小。如果你的数据集包含多个关节,建议按关节名称分别设定上下限,不要一刀切。
3.2 训练集与测试集划分:同一个患者的数据不能两边跑
这个坑在课程设计和毕业设计里特别常见,甚至很多论文里的实验也是这么翻车的。如果数据集包含多次就诊记录,且同一患者出现在多条数据里,划分训练集和测试集时如果把同一患者的记录同时放进两边,就会造成数据泄漏,评估结果虚高。
症状评估模型或分级模型,本质是在学习“什么样的数据对应什么样的评估等级”。如果同一个患者的相似数据同时出现在训练集和测试集,模型相当于带着答案考试,泛化性能根本没验证出来。
from sklearn.model_selection import GroupKFold # 以患者ID为分组依据进行交叉验证 X = df_clean.drop('评估等级', axis=1) y = df_clean['评估等级'] groups = df_clean['患者ID'] # 5 折分组交叉验证 group_kfold = GroupKFold(n_splits=5) for train_idx, test_idx in group_kfold.split(X, y, groups): X_train, X_test = X.iloc[train_idx], X.iloc[test_idx] y_train, y_test = y.iloc[train_idx], y.iloc[test_idx] # 确保同一患者不会出现在训练集和测试集 train_patients = set(groups.iloc[train_idx]) test_patients = set(groups.iloc[test_idx]) assert train_patients.isdisjoint(test_patients), "患者ID泄漏!" print(f"训练集患者数: {len(train_patients)},测试集患者数: {len(test_patients)}")用GroupKFold替代普通的train_test_split是最直接的解法。普通随机划分不关心分组,同组数据会被打散;分组划分保证每个患者的数据只出现在训练侧或测试侧任意一边。这个细节在答辩时是加分项,因为很多评审老师会直接问“你怎么保证数据没有泄漏”。
如果你只是做统计分析,不做模型训练,数据泄漏问题影响不大。但只要你尝试用机器学习方法做康复等级预测,分组划分就必须严格执行。这是我从实际项目里摔出来的教训,第一次做的时候没注意,模型准确率 95%,后来重跑才发现同一患者在两边,修正后掉到 78%,那个 95% 真的是自嗨。
4. 核心评估模块拆解:ROM 计算、肌力分级与 Barthel 指数
这一章直接进入代码实现。康复评估系统的核心算法没有复杂的数学公式,更多是把骨科和康复医学里的评分规则翻译成代码逻辑。重点在于规则要写对、边界要处理干净。
4.1 关节活动度计算:角度换算与健患侧对比
ROM(Range of Motion)计算是康复评估最基础的模块。原始数据通常是角度测量值,有时是直接从量角器读出的数值,有时是传感器给出的原始角度。计算逻辑本身不复杂,复杂在于角度的参考系。
比如肩关节屈曲,正常范围 0-180 度;测量时患者站立或坐位,量角器轴心在肩峰,固定臂平行于躯干,移动臂平行于上臂。系统里你看到的“健侧角度”是健康侧的活动度,“患侧角度”是受损侧的活动度。评估时要算两个指标:患侧绝对活动度和健患侧差值。绝对活动度判断关节是否受限,差值反映障碍程度。
def calculate_rom(rom_data): """计算关节活动度评估结果 Args: rom_data: dict,包含健侧角度和患侧角度 例如 {'肩关节屈曲': {'健侧': 165, '患侧': 92}} Returns: dict: 包含各关节绝对活动度、差值、受限等级 """ results = {} for joint, angles in rom_data.items(): healthy = angles['健侧'] affected = angles['患侧'] # 绝对活动度分级:按康复医学常见标准 if affected >= 120: level = '正常' elif affected >= 90: level = '轻度受限' elif affected >= 60: level = '中度受限' else: level = '重度受限' # 健患侧差值评估 diff = healthy - affected results[joint] = { '健侧角度': healthy, '患侧角度': affected, '差值': diff, '受限等级': level } return results分级阈值这里写的是我处理康复数据时常用的参考标准。不同关节的正常范围不一样,腕关节屈曲 90 度就算正常,肩关节要到 120 度以上才够,所以实际上阈值应该按关节配置。如果你要做得严谨,把每个关节的正常下限定义成配置项,不要写死在函数里。我曾见过一个改造版本把所有关节都用同一套阈值,膝关节屈曲 90 度被判成正常,实际上膝关节屈曲正常应到 130-150 度,这个版本明显是有问题的。
4.2 肌力分级:0-5 级规则判定与语义判断
肌力分级(MMT,Manual Muscle Testing)是康复评估里另一个核心模块。临床上是康复治疗师通过手法测试给患者肌肉力量打分,从 0 级完全瘫痪到 5 级正常肌力。这个分级听起来很主观,但实际有明确的操作定义:0 级无收缩,1 级有轻微收缩但不能带动关节活动,2 级能水平移动但不能抗重力,3 级能抗重力但不能抗阻力,4 级能抗部分阻力,5 级能抗充分阻力。
代码实现的关键是把这些临床描述映射成可选值或分级数字。有些数据集直接记录等级数字,有些则记录文字描述如“可抗重力”“可抗部分阻力”。做成一个映射字典,然后批量转换。
# 肌力分级映射字典 strength_mapping = { '无收缩': 0, '有收缩无关节活动': 1, '水平移动不能抗重力': 2, '抗重力不能抗阻力': 3, '抗部分阻力': 4, '抗充分阻力': 5 } def classify_strength(text_description): """将文字描述转为肌力等级""" return strength_mapping.get(text_description.strip(), None) # 批量处理数据集 df['肌力等级'] = df['肌力描述'].apply(classify_strength) print(df['肌力等级'].value_counts().sort_index())这个映射看起来简单,但在真实项目里有一个很烦的坑——同一个等级的描述在不同记录人写法上会有差异。“抗重力不能抗阻力”和“能抗重力,不能抗阻力”“抗重力,不抗阻力”是同一个意思,但字符串匹配时会漏掉。常见的做法是先把文本归一化,去掉标点和空格,再做映射。更省事的是直接用in判断核心关键词:
def classify_strength_fuzzy(text): """基于关键词的模糊分级""" if text is None: return None if '无收缩' in text: return 0 if '收缩' in text and '关节活动' not in text: return 1 if '水平' in text or '不能抗重力' not in text: return 2 if '抗重力' in text and '阻力' not in text: return 3 if '部分阻力' in text: return 4 if '充分阻力' in text or '完全阻力' in text: return 5 return None模糊匹配在关键词命中时更稳定,但要小心优先级顺序,比如“能抗部分阻力”同时也包含“抗重力”,必须先判断部分阻力再判断抗重力,否则会返回 3 级而不是 4 级。这类分级映射逻辑建议在课程设计说明里写清楚,因为这是评估系统的核心规则之一。
4.3 Barthel 指数评分:各项加权与等级判定逻辑
Barthel 指数(BI)是康复科最常用的日常生活活动能力评估量表,总分 100 分,分 10 个评定项。每一项得分不是连续值而是阶梯值,代码实现时要把每个项目的得分选项定义准确,同时依据总分划分功能等级:100 分独立,60-99 分基本独立但部分需要帮助,41-59 分中度功能障碍,21-40 分重度功能障碍,0-20 分完全依赖。
def calculate_barthel(row): """计算 Barthel 指数总分与功能等级 Args: row: dict,包含十项评分值 Returns: dict: 总分、等级、各分项得分 """ # Barthel 十项及各自的最高分 barthel_items = { '进食': 10, '洗澡': 5, '修饰': 5, '穿衣': 10, '控制大便': 10, '控制小便': 10, '如厕': 10, '床椅转移': 15, '平地行走': 15, '上下楼梯': 10 } total_score = 0 details = {} for item, max_score in barthel_items.items(): score = int(row.get(item, 0)) # 防御:得分不应超过该项最高分 score = min(score, max_score) # 得分不应低于 0 score = max(score, 0) details[item] = score total_score += score # 功能等级划分 if total_score == 100: level = '完全独立' elif total_score >= 60: level = '基本独立' elif total_score >= 40: level = '中度功能障碍' elif total_score >= 20: level = '重度功能障碍' else: level = '完全依赖' return { 'total_score': total_score, 'level': level, 'details': details }这段代码里最值得注意的其实是“床椅转移”和“平地行走”两项最高分是 15 分,其他大多数项是 10 分或 5 分。很多第一次接触 Barthel 的人会默认所有项满分相同,直接把十项原始得分加总,算出来的总分上限变成 100 之外的数。实际上标准 Barthel 各项满分是 5/10/15 混着的,总分封顶 100。你如果拿到一份数据集,里面的单项目分值不是整数倍,就要核对一下是不是已经经过了加权换算。
单项得分防御处理也值得保留。临床数据里偶尔会出现某项得分超过该项最高分的情况,比如进食填了 12 分,这种数据应该在计算时就截断,同时打日志提醒,而不是让它直接污染总分。
5. 先把坑踩平:数据集、依赖和运行时的常见问题
拆过不少开源项目,凡是带数据集的 Python 项目,翻车点高度集中。这里把我遇到的高频问题整理成清单,每条写清楚现象、原因和解决方案,照着排查能省下大量调试时间。
5.1 中文乱码和编码错误
现象:读取 CSV 后,列名出现\ufeff前缀,或者UnicodeDecodeError: 'utf-8' codec can't decode byte。
原因:临床数据多数时候是从 Excel 另存为 CSV 生成的,Excel 在 Windows 上保存 CSV 默认带 BOM(Byte Order Mark),且编码可能不是 UTF-8 而是 GBK 或 GB2312。直接用默认编码读取就会出问题。
解决:读取时显式指定编码。优先试utf-8-sig,不行就试gbk,再不行用encoding='gb18030',这是 GB 系列里支持字符最全的编码。我在处理康复数据集时,会做一个自动编码探测,先试 UTF-8 再回退 GBK,减少手动试错的次数。
def read_csv_auto_encoding(filepath): """自动探测并读取 CSV 文件""" for encoding in ['utf-8-sig', 'gbk', 'gb18030']: try: df = pd.read_csv(filepath, encoding=encoding) return df except (UnicodeDecodeError, UnicodeError): continue raise ValueError(f"无法解码文件: {filepath}")注意utf-8-sig不是只在有 BOM 时才能读,无 BOM 的 UTF-8 文件它也能正常读取,所以放在第一优先位置是安全的。
5.2 安装依赖后 import 仍然报错
现象:按 requirements 安装完依赖,运行 main.py 提示ModuleNotFoundError: No module named 'sklearn'或No module named 'PyQt5'。
原因:最常见的是当前 Python 环境和 pip 安装目标不是同一个环境,特别是同时装了 Anaconda 和系统 Python 时,出现了双环境或多虚拟环境混用。另一个原因是当前深度学习框架冲突导致的依赖版本不兼容。
解决:先确认 import 时用的是哪个 Python。在命令行里分别执行where python或which python,看当前激活环境;再用python -m pip install -r requirements.txt而不是pip install -r requirements.txt。前者能确保装进当前正在用的解释器。
# 确认当前 Python 解释器路径 which python # 用当前解释器对应的 pip 安装依赖 python -m pip install -r requirements.txt # 验证关键依赖是否可导入 python -c "import pandas, numpy, matplotlib; print('deps ok')"如果项目里带了机器学习相关依赖(scikit-learn 之类),还要注意 numpy 版本兼容问题。scikit-learn 和 pandas 对 numpy 的版本有要求,装多了容易把 numpy 升到不兼容版本,运行时报AttributeError或者ValueError: Object arrays are not supported。这种时候不用慌,把 numpy 降到报错提示里的建议版本即可。
5.3 运行时报 KeyError,列名对不上
现象:代码里写的是df['患者ID'],运行时报KeyError: '患者ID'。
原因:最常见的是读入数据的列名里带了空格或 BOM 前缀,实际列名是' 患者ID'或'\ufeff患者ID'。另一种可能是原始数据表用了不同的列名,比如写成'PID'或'patient_id',而代码没有做列名映射。
解决:读入数据后先打印列名列表,看实际长什么样,再做统一映射。我一般会做一个列名标准化函数,把列名里的空格、特殊字符清掉,再统一成代码里用的命名规范。
df = pd.read_csv('data/raw/barthel_scores.csv', encoding='utf-8-sig') # 查看实际列名 print(list(df.columns)) # 标准化列名:去掉首尾空格、把中文空格替换为下划线 df.columns = [str(col).strip().replace(' ', '_') for col in df.columns] # 重命名成代码使用的规范 df = df.rename(columns={ '患者ID': 'patient_id', '关节名称': 'joint_name', '健侧角度': 'healthy_angle', '患侧角度': 'affected_angle' })列名问题要建立成本能反应,英文项目是 snake_case,中文项目是拼音或直译,标准不统一。拿到数据集第一时间打印列名,比跑起来报错再去猜高效得多。
5.4 数据集里评估等级列带有噪声
现象:评估等级列里出现'正常 '、'正常(右)'、'Normal'、'3级'等多种写法,直接统计分类时数据看起来分布很奇怪。
原因:康复评估数据是多人录入的,没有统一的枚举约束。不同人写的描述不完全一样,类似但不同的字符串被当成不同类别。
解决:先做类别归并,把常见变体统一成一个标准值。比如把'正常 '(带空格)清成'正常',把括号备注从等级文本里去掉。
# 等级归一是挖掘前必须做的一步 def normalize_level(value): if pd.isna(value): return None text = str(value).strip() # 去掉括号及其中内容 text = text.split('(')[0].split('(')[0] mapping = { 'Normal': '正常', 'normal': '正常', 'N': '正常', } text_upper = text.upper() for raw, standard in mapping.items(): if text_upper == raw.upper(): return standard return text df['评估等级'] = df['评估等级'].apply(normalize_level) print(df['评估等级'].value_counts())归并之后再看类别分布,你可能会发现某些类别样本极少,只有几条。这类样本在训练模型时会导致类别不平衡,需要做上采样或直接放弃该类别。这一步处理直接影响后续评估结果的合理性。
5.5 matplotlib 画不出中文
现象:图表标题和坐标轴标签显示成方块□□□,或者报字体缺失警告。
原因:matplotlib 默认字体是 DejaVu Sans,不支持中文。康复评估项目大量使用中文标签,不改字体基本没法看。
解决:用参数配置强制指定中文字体。Windows 上常见的是 SimHei,Mac 上是 PingFang SC 或 Heiti TC。代码里直接设置。
import matplotlib.pyplot as plt # 设置中文字体 plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题这个配置要写在所有绘图代码之前。第二个参数axes.unicode_minus不设置的话,坐标轴上负号会显示成乱码。经验之谈:即使配好字体,不同操作系统的字体名不一样,代码放到别的机器上可能又不生效,所以绘图模块里最好带一个按系统自动选择字体的工具函数。
6. 从能用走向可信:评估一致性验证与结果可视化
评估系统做完算法,跑出结果,这只能算“能用”。在课程设计和毕业设计里,真正拉开差距的是你有没有验证评估结果的可靠性。这一章我给出一个我常用的验证路径:用一致性检验评估结果是否可信,再加可视化让结果一目了然。
康复评估和很多工程测量不同,它的“真实值”来自康复治疗师的判断,没有金标准。所以验证的核心不是看准确率,而是看你的系统评估结果和人工评估结果是否一致。常用的指标是加权 Kappa 系数,适合等级数据的吻合度检验。
from sklearn.metrics import cohen_kappa_score # system_level: 系统自动评估的等级 # manual_level: 治疗师人工评估的等级 # 两者都是等级序列,如 0-5 级的整数 kappa = cohen_kappa_score(system_level, manual_level, weights='linear') print(f"加权 Kappa: {kappa:.3f}") # 一致性解读 if kappa >= 0.8: agreement = "极好" elif kappa >= 0.6: agreement = "较好" elif kappa >= 0.4: agreement = "中等" else: agreement = "较差" print(f"一致性水平: {agreement}")加权 Kappa 用weights='linear'是因为等级误差的严重程度不同。比如肌力分级,系统给 3 级人工给 4 级,与系统给 1 级人工给 4 级,前者误差显然更容易接受。线性权重正好体现“相邻等级误差比远端等级误差影响小”。这个细节在毕设里写上一句,比单纯贴准确率有说服力得多。
结果可视化方面,我建议至少做两个图。第一个是健患侧角度对比柱状图,每个关节两根柱子并排,一眼看出活动受限程度。第二个是肌力等级分布直方图,或者 Barthel 总分分布箱线图。
import matplotlib.pyplot as plt import numpy as np # 健患侧角度对比 joints = list(rom_results.keys()) healthy_vals = [rom_results[j]['健侧角度'] for j in joints] affected_vals = [rom_results[j]['患侧角度'] for j in joints] x = np.arange(len(joints)) width = 0.35 fig, ax = plt.subplots(figsize=(10, 6)) bars1 = ax.bar(x - width/2, healthy_vals, width, label='健侧') bars2 = ax.bar(x + width/2, affected_vals, width, label='患侧') ax.set_xlabel('关节') ax.set_ylabel('活动角度(度)') ax.set_title('健患侧关节活动度对比') ax.set_xticks(x) ax.set_xticklabels(joints, rotation=45) ax.legend() ax.grid(axis='y', linestyle='--', alpha=0.3) plt.tight_layout() plt.savefig('output/rom_comparison.png', dpi=150) plt.show()柱状图的关键在于同一个关节的健侧和患侧柱子紧挨着,中间留出组间空隙,这样对比关系才清晰。保存图片时设置dpi=150是为了插入论文或答辩 PPT 时足够清晰,默认 100 会显得糊。
最后我想说一个关于数据可信度的习惯。我在做一个骨折术后康复评估项目时,系统算出来的 Barthel 等级和治疗师评估出了冲突,翻查数据源发现,患者数据是家属代填的,有几项得分明显偏高。从那以后我每次做完评估结果,都会强制走一遍一致性检验和可视化对比,不只看数值,还要看分布是否合理。数据本身不会告诉你它有问题,只有你带着检查的意识去看,问题才会浮现。希望这套拆解和踩坑清单能帮你顺利跑通这个康复评估项目,少走几段弯路。
本文还有配套的精品资源,点击获取