简介:压缩包内含基于Python实现的康复评估系统,适合计算机、人工智能、自动化等专业学生用于毕业设计、课程设计或项目初期演示;系统以人体运动捕捉数据为核心,动作捕捉文件记录骨骼运动序列,表格数据保存评估指标,源代码与交互式笔记共同构成核心算法与可视化流程,配套说明文档则提供环境与运行指引,整体结构清晰,便于二次开发。全套共157个文件,压缩后约16.18MB,资源轻量而完整,数据、代码与文档分布明确,读者可快速定位所需模块;已有260人次浏览学习,适合需要从原始动作数据到评估结果全流程实践的学生。参考交互式笔记中的分析与函数定义,可掌握动作文件解析、评估指标计算及结论输出方法;也可将表格数据替换为自有数据,扩展至其他姿态或康复评估场景。其兼具教学与实用价值,是系统掌握康复评估建模的良好起点。
1. 康复评估系统不只是一张打分表:Python 源码与数据集怎么串起从评估到结论的整条链路
康复评估系统在临床科室里最常见的形态,是一堆纸质量表和 Excel 表:Fugl-Meyer 评估表、Barthel 指数、Berg 平衡量表,每个条目一个分数,最后由人工汇总算总分、定分级。手算 30 多个条目的量表时,漏记、算错、复查找不到填写依据都是家常便饭。这个项目标题背后,是一套用 Python 实现的康复评估系统源码,外加一份配套数据集。它的价值在于把“采集条目—筛选分数—判定结果”这条路变成可重复运行的代码,同时让评估数据能被追溯、对比和再分析。适合康复科信息化建设、科研数据整理,以及想从零搭建临床评估工具的开发者。它不是把量表做成网页表单那么简单,重点是数据结构、评分规则和数据集怎么组织——抄代码容易,抄到靠谱的数据模型才能少返工。
2. 康复评估数据集是系统的地基:字段设计、两种存储形态与标注质量
动手写评分逻辑之前,我一般会先把数据集摸透。康复评估系统和普通业务系统的最大区别,是它每一步都依赖“结构化程度很高”的临床数据:一次评估涉及患者、评估师、量表、条目、分数、时间,甚至还包括运动传感器采集的连续信号。如果一开始字段设计得随意,后面每次加量表、加评估项目都要改表和改解析代码,这种返工很容易让人翻车。
2.1 一次评估记录有哪些字段:患者、评估师、量表与条目的四层字段
先把一次评估拆成四个层次。第一层是患者信息,只需要一个稳定的 patient_id,姓名、年龄、诊断这类信息不应该塞进评分表里;第二层是评估记录本身,包括评估师、评估时间、评测轮次;第三层是量表信息,比如用的是哪一个版本的 Fugl-Meyer,或者哪一种 Barthel 指数版本;第四层才是具体的条目分数,即每个量表的每一个评分项对应的得分。
这里最容易做错的是把量表条目的分数直接平铺成 dataset 的每一列,例如列名叫 score1、score2、score3。短时间看好像省事,一旦两份量表的条目数量不一致,或者同一量表不同版本的条目有增删,你的全部数据处理代码都要跟着改。我更推荐把“条目分数”做成一个字典,通用字段放外层,条目字段用统一前缀在前面:
# assessment_record.py from dataclasses import dataclass from datetime import datetime from typing import Dict, Optional @dataclass class AssessmentRecord: patient_id: str # 患者编号,关联患者基础信息表 assessor_id: str # 评估师编号,用于评分一致性分析 instrument: str # 量表标识,如 "FM_UE" 或 "BARTHEL" session_no: int # 评测轮次,第几次评估 scores: Dict[str, int] # 条目名 -> 分数,例如 {"shoulder_flex": 2} assessed_at: datetime # 评估时间,用于康复进程对比 remark: Optional[str] = None # 备注,记录评估中的异常情况这段代码把一次评估记录变成一个 dataclass 对象,scores 用 dict 保存条目分数,而不是在类里写死 30 个字段。这样做的好处是,你的评分引擎只依赖 scores.key 来取值,新增量表时不需要修改数据模型。patient_id、assessor_id 和 session_no 三个字段是数据集的灵魂:patient_id 让你能做纵向对比,assessor_id 让你能查评估师之间的打分一致性,session_no 让你能画出每次复评的变化曲线。
2.2 CSV 与 JSON 怎么选:扁平表格适合批处理,嵌套结构适合量表扩展
数据集文件最常见的形态有两种:CSV 和 JSON。CSV 的好处是 Excel 能直接打开、pandas 能直接读,适合做批量统计和训练集;但 CSV 的劣势是嵌套结构很难表达,一个量表的条目分数只能平铺成几十列,列一多,字段对齐就容易出错。JSON 则相反,嵌套的 scores 字典天然匹配上面的数据结构,扩展起来不用改表头。
我处理这类项目时的习惯是:原始数据用 CSV 保存,因为采集端和评估师更容易维护;程序内部统一用 AssessmentRecord 对象;需要对外交付或做接口传输时再转 JSON。CSV 的表头需要和代码约定一致,固定字段直接命名,条目字段统一用 item_ 前缀:
import csv from datetime import datetime from typing import List def load_csv_to_records(path: str) -> List[AssessmentRecord]: records = [] with open(path, encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: score_item = {} for k, v in row.items(): if k.startswith("item_"): # 只收集条目字段 score_item[k] = int(v) # 分数统一转成 int records.append(AssessmentRecord( patient_id=row["patient_id"], assessor_id=row["assessor_id"], instrument=row["instrument"], session_no=int(row["session_no"]), scores=score_item, assessed_at=datetime.fromisoformat(row["assessed_at"]), )) return records这段代码解决的是“把 CSV 读成可计算的评估对象”。用 csv.DictReader 读出来的每一行是一个字典,代码里只筛 item_ 前缀的列,其余列按字段名映射。为什么用 utf-8-sig?因为评估数据经常从 Excel 导出,带 BOM 的文件直接用 utf-8 解析会在第一列列名前面多出一个隐藏字符,导致匹配不上。至于条目字段,我见过很多数据集把分数填成中文“通过/不通过”,读进来直接 int 转换报错,所以数据在源头必须约定为数字,这一步宁可早报错也不要晚报错。
2.3 数据标注别只填分数:缺失、越界与评估师一致性
有了字段和文件格式,真正决定评估系统能不能落地的是标注质量。康复数据集的标注不是简单的“给分”,它有三个容易出问题的点:缺失、越界、评估师不一致。
缺失的典型场景是患者在某个条目上配合不了,评估师留空。代码读到空值如果直接转 int,整批数据加载就中断;如果跳过,最终总分计算又会失真。我的处理方案是留空的条目不计入总分,同时单独统计缺失条目数量,在报告里明确标注“未评估项 x 条”。越界则是数据录入时把 0-2 分的条目打成了 3 分,这类脏数据要靠数据校验规则兜底,在加载时直接报错并提示是哪一行哪一列。而评估师之间评分不一致的问题,靠代码只能做检测:按 assessor_id 分组统计同一个患者同一次评估的分数差异,差异过大时在质控报告里标红。很多从网上下载的免费 python 源码大全里的评估工具都忽略这一点,没有 assessor_id,也就永远没法知道分数波动是病情变化还是评估师手松手紧。
3. 康复评估系统的评分逻辑:从量表配置到分数判定的代码路径
数据加载完之后,下一步就是把评分规则从纸质量表变成代码。很多半路出家的项目把分数计算写死在函数里,一个量表一个函数,改一处逻辑要动整个文件。我对这类系统的要求是:量表是配置,计算是引擎。这样哪怕科室换了一个评估量表版本,你只需要改配置字典,评分引擎一行都不用动。评分逻辑如果不拆开,整个系统就是个黑匣子,临床人员根本不敢信你的输出结果。
3.1 量表评分规则的代码化思路:先映射,再聚合,后判定
把纸质量表变成代码,本质上是三步。第一步是映射,把量表的每个条目名、可选分值、满分值写进一个配置字典;第二步是聚合,把用户填写的条目分数按量表规则累加;第三步是判定,根据总分或占比落到预设的分级区间,比如正常、轻度、中度、重度。
关键在于判定标准不要写死在计算函数里,因为不同医院、不同评估量表版本,分级阈值可能不同。把阈值放进配置字典里,将来调整只改一处。用代码来表现就是:
# instruments/fugl_meyer_ue.py FUGL_MEYER_UE = { "instrument_id": "FM_UE", "name": "Fugl-Meyer 上肢运动功能评估", "total_score": 66, # 上肢部分满分 66 分 "items": { "shoulder_reflex": {"label": "肱二头肌/肱三头肌反射", "max": 2}, "shoulder_flex": {"label": "肩关节屈曲", "max": 2}, "shoulder_ext": {"label": "肩关节伸展", "max": 2}, # 实际量表有 33 个条目,按你的量表标准补全即可 }, "grade_cfg": [ {"low": 0.96, "high": 1.00, "label": "无明显障碍"}, {"low": 0.85, "high": 0.95, "label": "轻度障碍"}, {"low": 0.50, "high": 0.84, "label": "中度障碍"}, {"low": 0.00, "high": 0.49, "label": "重度障碍"}, ], }这个配置字典里每一项的含义很直接:items 里的每个条目,max 表示这个条目的满分,评估时给 0、1、2 分;grade_cfg 的分级用的是占比而不是绝对分数,因为 Fugl-Meyer 上肢部分满分 66 分,直接用基于 100 分的临床分级阈值会错位。实际使用中,我见过有的科室要求按原版标准判定,有的按中国康复医学会修订版判定,两种标准阈值是不同的,所以我一般把 grade_cfg 单独放到配置文件中,方便科室自己调整。
3.2 用 Python 实现 Fugl-Meyer 上肢评分的完整代码
有了上面的配置字典,评分引擎就可以写得非常通用。它只需要接收一条 AssessmentRecord 和一个量表配置,输出总分、百分比、分级和明细:
def score_assessment(record: AssessmentRecord, instrument: dict) -> dict: total = 0 missing_items = [] detail = {} for item_key, spec in instrument["items"].items(): user_score = record.scores.get(item_key) if user_score is None: missing_items.append(item_key) detail[item_key] = {"obtained": None, "max": spec["max"], "note": "缺项"} continue if user_score < 0 or user_score > spec["max"]: raise ValueError(f"{item_key} 分数 {user_score} 超出 0-{spec['max']} 范围") total += user_score detail[item_key] = {"obtained": user_score, "max": spec["max"]} # 用已评估条目的占比来判定分级 evaluated_max = sum(spec["max"] for k, spec in instrument["items"].items() if k not in missing_items) pct = round(total / evaluated_max, 3) if evaluated_max > 0 else 0.0 grade = None for cfg in instrument["grade_cfg"]: if cfg["low"] <= pct <= cfg["high"]: grade = cfg["label"] break return { "record_id": record.patient_id, "total": total, "pct": pct, "grade": grade, "missing": missing_items, "detail": detail, }这段代码的核心逻辑是先用 dict.get 取分,而不是直接 record.scores[item_key],这样缺项不会直接抛 KeyError,而是被记录到 missing_items 列表里。分数越界的检查放在累加之前,出现脏数据马上干预,避免错误分数进入统计。分级判定用的是“已评估条目分数占比”,这和直接把总分除以量表满分是两个概念——假设患者缺了 10 个条目,用总分除以 66,他的百分比会偏低 15 个百分点,从而被错误地划入更严重一档;用已评估满分做分母,才能保证缺失项不干扰严重程度的判断。
3.3 参数说明:评分阈值、权重与归一化策略
这里有两个参数容易被忽略。第一个是 grade_cfg 的阈值,示例里的 0.96、0.85、0.50 是通行临床分级习惯的近似值,但不同版本的量表手册可能给出完全不同的界值,动手做项目前务必让科室提供书面评定标准,不要自己在网上搜一个阈值就用。第二个是分数的归一化方式,上面用的是“已评估条目占比”,它的底层逻辑是假设缺失条目不影响最终分级,这在患者因疼痛、疲劳无法完成部分动作时是合理的;但如果患者是因为认知障碍完全无法配合,缺项占比过高时,整个分级就没意义了。我一般会在返回结果里加一个判断:missing 条数超过总量的 20% 时,grade 字段强制置为“数据不足,无法判定”。这样报告不会输出一个看似精确、实则误导的结论。
4. 运动数据接入与关节活动度计算:从传感器到可画的康复趋势曲线
康复评估系统不能只处理量表分数,还要处理运动数据。量表是主观的、离散的,而关节活动度、步速、平衡参数是客观的、连续的。常见的做法是用 IMU 传感器或单目摄像头采集动作,然后由代码算出关节角度、动作次数和活动范围。如果你走的是“用摄像头自动评估”路线,通常会先用类似目标检测工具识别关键点、训练自己的康复动作数据集,但那已经是另一个完整项目了。这个标题下的数据集,更多是量表分数加运动测量值,运动数据的处理重点在清洗和特征提取。
4.1 运动数据的常见形态:IMU 记录、关节角度与视频标注
运动数据有三种常见形态。第一种是 IMU 原始记录,包含加速度、角速度和时间戳;第二种是关节角度序列,可能是传感器算法直接输出的欧拉角,也可能通过姿态估计模型从视频里提取的关键点坐标换算;第三种是视频文件外加关键动作标注。三种形态里,IMU 原始记录最抗造,因为它不依赖拍摄环境,但噪声大;关节角度序列最直观,直接反映功能,但来源算法不同,数值可比性差;视频数据信息量最大,却也最占空间、最难清洗。
我建议你从关节角度序列入手,因为它的计算链路最短:拿到角度随时间变化的数组,滤波,找极值,算活动度。这个链路跑通之后,再去接 IMU 原始数据或者视频关键点,思路是一样的,只是把角度来源换成你自己的数据解析模块。
4.2 用 Python 计算关节活动度的代码实现与滤波参数
关节活动度的定义很简单:在一次完整的关节屈伸动作里,最大角度减去最小角度。困难在于原始信号抖动,直接取极值会把一次肌肉震颤也算成活动范围。我的做法是先用一阶指数平滑滤波去掉高频抖动,再求极值:
import numpy as np def ema_filter(signal, alpha=0.2): """一阶指数平滑滤波,alpha 越小越平滑,但滞后越大""" signal = np.asarray(signal, dtype=float) filtered = np.empty_like(signal) filtered[0] = signal[0] for i in range(1, len(signal)): filtered[i] = alpha * signal[i] + (1 - alpha) * filtered[i - 1] return filtered def rom_from_angles(angles, alpha=0.2, min_amp=10.0): smooth = ema_filter(angles, alpha) max_angle = float(np.max(smooth)) min_angle = float(np.min(smooth)) rom = max_angle - min_angle if rom < min_amp: raise ValueError("信号幅值过小,请检查传感器佩戴与标定") return { "rom_deg": rom, "max_angle_deg": max_angle, "min_angle_deg": min_angle, }几个关键参数值得细说。alpha 是滤波系数,取 0.1 到 0.3 之间比较合适:alpha 越大,滤波越跟随原始信号,噪声抑制差;alpha 越小,曲线越平滑,但真实活动度的峰值会被削掉,导致 ROM 偏小。这里取 0.2 是中等程度,适合采样率 50Hz 到 100Hz 的关节角度信号。另一个参数是 min_amp,单位是度,用来兜底传感器没戴好、标定错误、数据全为零等异常场景,一旦算出来的 ROM 小于这个值,直接报错而不是输出一个无意义的结果。还有一点必须提醒:这套代码只适用于“角度序列里只有一次完整动作”的场景;如果患者连续做了好几个动作,必须先按静止段切分,否则最大角减最小角会把不同动作的边界频谱混进来,ROM 计算必然失真。
4.3 康复进程可视化:把多次评估画成曲线对比
有了单次评估的活动度数据,下一步就是纵向对比。把同一个患者不同 session 的 ROM、量表总分、分级结果放在同一张图上,康复效果一眼就能看出来。可视化代码用 matplotlib 就够,不需要上重型框架:
import matplotlib.pyplot as plt def plot_rehab_progress(sessions, labels, title="康复进程趋势"): """sessions 为按时间排序的指标列表,labels 为指标名称""" plt.figure(figsize=(10, 5)) x = list(range(1, len(sessions) + 1)) for i, s in enumerate(sessions): plt.plot(x, s, marker="o", label=labels[i]) plt.xlabel("评测轮次") plt.ylabel("指标值") plt.title(title) plt.legend() plt.grid(alpha=0.3) plt.show()这段函数接收多个指标序列,例如肩关节屈曲 ROM 序列、Fugl-Meyer 总分序列,画在同一张图上。grid 的 alpha 设成 0.3 是为了让网格线不喧宾夺主;marker 用圆形点是为了在数据点少的时候能看清每一轮的具体数值。真正在报告里用的时候,我一般不直接 plt.show(),而是把图保存成 PNG 再嵌入评估报告,这样临床医生打印和归档都方便。
5. 康复评估系统落地避坑:5 条容易让结果集体失真的血泪经验
把代码跑通只是第一步,真正在医院环境里让评估系统被认可,靠的是把坑填平。下面这几条是我在类似系统上反复踩过的,每一条都能让结果集体失真,比单个 bug 严重得多。
5.1 缺项直接抛异常:一套评估跑不下去
现象:一条评估记录缺了某个条目分数,评分引擎直接报错中断,整个患者的报告生成流程卡死。原因:早期代码直接用 record.scores[item_key] 取分,缺项就抛 KeyError;或者用了 int(None) 直接类型报错。解决:评分引擎统一用 dict.get(item_key) 配合缺失值标记,缺项不计分但也不中断;报告生成时对缺失项显示“未评估”,并在结果里单独列出缺项清单。血泪总结是:医疗数据永远比你想的更残缺,不要假设每一条记录都完整。
5.2 同一个患者两次评分差十几分:评估师不一致才是主要误差来源
现象:同一个患者隔三天复评,两位评估师打分相差 8 到 12 分,系统画出来的康复趋势曲线像过山车。原因:不是算法错了,而是评估师对量表条目的理解不一致,还有人没用标准口令引导患者。解决:数据集里保留 assessor_id,按评估师分组做一致性分析;对差异大的条目,在量表校验规则里给出口令提示;每次评估完成后让另一位评估师抽测复核。代码解决不了主观性,但代码能把主观性暴露出来,逼着团队统一操作标准。
5.3 几十个病例就上深度学习:小样本场景下的模型幻觉
现象:数据集只有几十个患者、几千条记录,有团队试图用神经网络直接预测康复评估结果,测试集表现忽高忽低,换一批数据就崩。原因:样本量太小,特征维度高,模型把个别患者的特殊性当成了规律。解决:优先用统计方法和规则引擎,比如线性回归、决策树、评分量表本身的判定逻辑;只有积累到上万条高质量记录时,再考虑训练端到端模型。康复评估场景里,“能解释”比“更聪明”重要得多,黑盒模型在临床上很难被信任。
5.4 开发机能跑、医院机器跑不了:Python 环境与依赖的离线清单
现象:代码在开发机上跑得顺畅,部署到医院内网机器上,pandas、numpy、matplotlib 一装就各种报错,折腾一整天还在解决环境问题。原因:内网机器没有外网,靠拷 whl 文件安装,版本不一致导致依赖冲突。解决:用 requirements.txt 锁定全部版本,提前在联网机器上用 pip download 把依赖 whl 全部拉齐,部署时指定 Python 版本统一安装。我在类似项目里吃过亏之后,现在习惯是把整个运行环境做成最小依赖,能不用重库就不用,降低交付时的环境适配成本。
5.5 PDF 报告中文乱码:报告生成链路上的字体坑
现象:评估报告生成后,中文全部变成方框或问号,打印出来根本没法归档。原因:PDF 生成库默认字体不包含中文字形,常见于直接使用 ReportLab 默认字体的情况。解决:改用 HTML 渲染再转 PDF 的链路,在 HTML 样式里显式指定中文字体文件路径,提前把思源黑体或系统中文字体打包进项目,部署时一并安装。这条坑往往在项目最后才出现,但影响的是临床人员对系统的第一印象。
6. 把康复评估系统做成可复用服务:FastAPI 端点、报告生成与我的默认配置
前面几章把数据模型、评分引擎和运动数据处理都串起来了,最后一步是让整个系统可以被团队其他成员调用。常见做法是用 FastAPI 把评分引擎暴露成服务,前端、Excel 工具、甚至是另一个 Python 脚本都能通过 HTTP 接口做评估。
6.1 用 FastAPI 暴露评估端点:一条命令把评估逻辑变成内部服务
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ScorePayload(BaseModel): record_id: str scores: dict @app.post("/assess/{instrument_id}") def assess(instrument_id: str, payload: ScorePayload): record = load_record(payload.record_id) instrument = load_instrument(instrument_id) result = score_assessment(record, instrument) return result这个端点接收量表标识和条目分数,返回总分、分级、缺项清单。把评估逻辑包成服务之后,数据录入端可以拿它做实时校验,评估师边录边看到结果,而不是等全部录完再统一算分。
6.2 评估报告的自动生成与结构设计
报告生成我习惯用 Jinja2 渲染 HTML,再用 WeasyPrint 转 PDF,核心是你提前把中文字体注册好,避免最后一公里翻车。报告结构一般分三段:患者基本信息与评测轮次、各条目的得分明细、以及纵向趋势图和分级结论。现在我的默认习惯是:所有阈值放进配置文件,所有依赖锁定版本,数据集保留 assessor_id 字段。我接过一个康复科的项目,第一版没有保留评估师字段,后来做一致性分析时发现数据完全没法追溯,只能重新收集。这个教训让我从此把“可追溯”当成评估系统的默认配置。如果你准备照这个方向做,先把数据模型设计好,把缺失值处理策略定好,再动手写评分引擎,会少走很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取