首页 / 资讯中心 / 文章详情

Python康复评估系统:从评分引擎到FastAPI部署与趋势分析

Python康复评估系统:从评分引擎到FastAPI部署与趋势分析 ★ FEATURED ARTICLE
简介一套基于Python实现的康复评估系统源码与配套数据集主要面向计算机、自动化、电子信息等专业学生用于毕业设计、课程设计或项目初期演示也适合Python进阶者参考。压缩包内共157个文件包含70个csv数据文件、68个bvh动作捕捉数据、11个py源码文件、2个ipynb演示笔记及其他辅助文件整体约16.18MB。bvh文件记录了不同受试者的康复动作序列csv可对应评估指标与样本标签py源码覆盖数据预处理、骨骼特征计算、评估模型构建与结果可视化等流程ipynb则提供分步演示便于理解和二次开发。目前已有260人学习下载。压缩包经过测试上传代码运行正常且目录结构清晰既能支撑康复评估功能的快速落地也方便在此基础上调整算法、改造界面或扩展新模块适合多元化的毕业设计与课程实践需求。1. 用 Python 做的康复评估系统源码先把纸质评估表改造成结构化数据一个基于 Python 的康复评估系统源码配着数据集一起给你解压后你得到的不是一份“能跑就行”的 Demo而是一套可以真正接进康复科日常流程的评估工具。我见过太多康复科室还在用纸质 FMA、Barthel 表治疗师手填、计算器算总分、再对着 Excel 翻历史对比前后数据版本还对不上。这个项目解决的就是这件事把康复评估里最核心的量表、评分、分级、报告和随访趋势做成本地可运行的 Python 服务配套的数据 CSV 可以直接导入数据库当成初始评估记录。适合康复科软件开发者、医疗信息化外包团队以及想拿医疗 Python 项目练手的人。2. 从 FMA、BBS 到 Barthel把康复量表翻译成可执行的评分引擎康复评估系统的第一道坎不是界面好不好看而是评分规则能不能在代码里立住。康复医学里的量表看起来就是打几分、加个总实际上每一个量表都有自己的条目数量、分值集合、版本差异和分级区间。你如果一上来就写if total 90: return 轻度后面换一个量表就全乱。常见做法是先做一个“模板化评估引擎”让每个量表都变成一份可配置的数据结构再让引擎统一处理算分、校验和分级。2.1 康复评估系统的核心一个能处理“0/1/2 打分”的评估引擎拿 Fugl-Meyer 运动功能评估FMA举例它把运动功能分成上肢 33 项、下肢 17 项每一项按 0、1、2 三档打分上肢满分 66、下肢满分 34合计 100。这个结构如果用一堆 if-else 写死在视图里后期维护就是灾难。我一般会先定义一份评估模板把条目 ID、题目文本、允许分值绑在一起# 一个最小可运行的 FMA 运动功能评估模板 from dataclasses import dataclass dataclass class ScaleTemplate: scale_id: str # 量表唯一标识比如 fma scale_name: str # 展示名称比如 FMA 运动功能版 items: dict # 条目ID - (题目文本, 允许的分值集合) max_total: int # 总分上限用于进度条和报告展示 grading: list # 分级规则按阈值降序排列 [(最小总分, 级别)] FMA_ITEMS { ue_01: (肩反射活动, (0, 1, 2)), ue_02: (屈肌共同运动肩后缩上提, (0, 1, 2)), ue_03: (屈肌共同运动肩外展≥90°, (0, 1, 2)), # 其余 30 项遵循同样模式完整条目建议单独放 items.py 里维护 }模板建好之后评分引擎只需要做三件事遍历条目、检查分值合法性、累加总分并分级。下面这个score_scale函数可以在所有量表之间复用而不是每个量表写一套算法def score_scale(template: ScaleTemplate, results: dict) - dict: results 的格式是 {条目ID: 分值}返回总分、分级和逐项错误列表。 errors [] total 0 for item_id, (text, allowed_scores) in template.items.items(): value results.get(item_id) if value is None: errors.append(f{item_id} 未作答) elif value not in allowed_scores: errors.append(f{item_id} 不允许分值 {value}允许 {allowed_scores}) else: total value if errors: return {ok: False, errors: errors} level 未知 for threshold, label in template.grading: if total threshold: level label break return {ok: True, total: total, level: level}这段代码的逻辑很直白但有两个参数细节要注意。第一是grading必须按阈值降序排列比如先判 96再判 85否则低于阈值的分数会落进错误的级别。第二是条目 ID 的命名建议用ue_01这种定长格式而不是ue_1否则在字典序排序显示时会出现ue_10排在ue_2前面的幻觉虽然不影响求和但治疗师看检查表时会懵。2.2 让模板可扩展Berg 平衡量表 56 分与 Barthel 100 分的分值集合FMA 的每条分值都是 0/1/2这是最规整的情况。真正让人踩坑的是 Barthel 指数这类“分值集合不统一”的量表。Barthel 一共 10 项有的项是 0/5有的项是 0/5/10有的项还有 15 分档比如“床椅转移”和“平地行走”就是 0/5/10/15。如果你在代码里只设一个总分上限 100那录入端就有可能把“洗澡”填成 10 分总分算出 105 还没报错治疗师当场就能发现不对。所以在模板数据结构里我坚持为每个条目单独存一份允许分值集合BARTHEL_ITEMS { ba_01: (进食, (0, 5, 10)), ba_02: (洗澡, (0, 5)), ba_03: (修饰, (0, 5)), ba_04: (穿衣, (0, 5, 10)), ba_05: (大便控制, (0, 5, 10)), ba_06: (小便控制, (0, 5, 10)), ba_07: (如厕, (0, 5, 10)), ba_08: (床椅转移, (0, 5, 10, 15)), ba_09: (平地行走, (0, 5, 10, 15)), ba_10: (上下楼梯, (0, 5, 10)), } BBS_ITEMS { bbs_01: (从坐位站起, (0, 1, 2, 3, 4)), bbs_02: (独立站立, (0, 1, 2, 3, 4)), bbs_03: (闭眼站立, (0, 1, 2, 3, 4)), # BBS 共 14 项每项 0-4 分总分 56 }这里的“参数说明”其实就一句话每一个条目的分值集合必须和原版量表逐项核对别相信网上转了好几手的表。Berg 平衡量表相对省心14 项全是 0 到 4 分总分 56按 0-20 高跌倒风险、21-40 中度风险、41-56 低风险来分级。Barthel 就必须一项一项对尤其“洗澡”这一项原版只有 0 和 5不存在 10 分档。当每个条目的分值集合进了模板之后前端就可以直接拿这份配置生成打分控件后端再用同一个配置做二次校验非法值在入口就被拦住而不是等总分算出来才发现不对劲。2.3 把分级存进配置为什么不要硬编码我在前面的代码里把grading放进模板这个习惯是从一次翻车里学来的。当时我把 FMA 的分级阈值直接写死在视图函数里三个月后康复科主任说“我们这里的患者人群偏重96 分轻度这个阈值要往下降”我只好满项目搜96这个数字。如果你一开始就把分级规则放进配置比如scale_config.json那治疗师调阈值就是改一个数字的事{ fma: { max_total: 100, grading: [[96, 轻度运动障碍], [85, 中度运动障碍], [50, 明显运动障碍], [0, 严重运动障碍]] }, bbs: { max_total: 56, grading: [[41, 低跌倒风险], [21, 中度风险], [0, 高风险]] }, barthel: { max_total: 100, grading: [[61, 轻度依赖], [41, 中度依赖], [21, 重度依赖], [0, 完全依赖]] } }分级阈值在不同康复机构、不同患者群里差异很大源码包里给的是通用参考值上线前一定要让治疗师按自己科室的规范确认一遍。提示FMA 的“96 分轻度、85 分中度、50 分明显、50 以下严重”只是常用分段不是教科书唯一标准。做系统时把默认值写进配置报告里标注“按科室共识版本”比留一个神秘数字体面得多。这一层做完评分引擎已经能处理绝大多数量表了。关键在于“模板驱动”新增一个量表不需要动引擎代码只需要加一份模板数据然后注册到量表字典里。3. 源码包怎么跑起来FastAPI 后端、SQLite 存储与前端的职责划分评分引擎只是系统的心脏外面还得裹上 API、存储和页面。这个源码包里最常见的技术栈是 FastAPI SQLAlchemy SQLite前端直接用 HTML 加 fetch 调用不引入重型框架。选这个组合的理由很现实康复评估系统通常部署在科室一台 Windows 电脑上数据量不大SQLite 零配置、单文件备份FastAPI 能在一个文件里把接口和静态页面都托管起来部署成本最低。3.1 解压后常见的目录结构哪一层放业务哪一层放数据我拿到这种源码包第一件事不是跑起来而是看目录结构。一个结构清楚的康复评估系统通常会按下面这种方式分层RehabAssessment/ ├── app/ │ ├── main.py # FastAPI 入口注册路由和中间件 │ ├── models.py # SQLAlchemy 表结构 │ ├── schemas.py # Pydantic 请求/响应模型 │ ├── services/ │ │ ├── scale.py # 评估引擎上一章的模板评分逻辑 │ │ ├── trend.py # 康复趋势整理 │ │ └── rom.py # 关节活动度计算进阶模块 │ ├── api/ │ │ ├── patients.py │ │ └── assessments.py │ └── static/ # 前端页面与 JS ├── data/ # 数据集 CSV ├── tests/ # 引擎单测 ├── requirements.txt └── README.md分层最关键的一条是services里只放纯业务不出现request、response这些词api层只做参数接收、调用服务和拼装响应。这样评分引擎可以被单元测试直接调用不用启动服务。很多人把业务写在路由函数里结果每个接口都在重复算分逻辑换量表时候改到崩溃。3.2 跑通最小链路提交评估答案到拿到结构化报告入口文件main.py一般不复杂但有几个配置位要注意。开发期一定要先把 CORS 中间件打开不然后端写完了前端页面照样调不通具体坑在第 5 章讲。这里先看最小入口from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app FastAPI(titlerehab-assessment-api) app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], )代码里allow_origins[*]是开发期最省事的写法生产环境要改成具体域名。接着看评估提交接口这是整个系统的关键链路前端把患者 ID、量表 ID、评估日期和所有条目的答案传进来后端调用评分引擎通过后写入数据库from pydantic import BaseModel class AssessmentIn(BaseModel): patient_id: str scale_id: str assessment_date: str # 统一传 YYYY-MM-DD results: dict # {条目ID: 分值} app.post(/api/assessments) def create_assessment(data: AssessmentIn): template scale_templates.get(data.scale_id) if template is None: return {ok: False, reason: f未知量表 {data.scale_id}} scored score_scale(template, data.results) if not scored[ok]: return {ok: False, errors: scored[errors]} # 入库一次评估会话 若干 detail 行用事务提交 return {ok: True, total: scored[total], level: scored[level]}这里 Pydantic 的results字段直接接收字典省去了逐个参数声明的啰嗦但代价是字段合法性得靠评分引擎自己查。所以顺序很重要先校验、再算分、最后入库千万别先落库再校验否则脏数据进去就不好洗了。启动服务的命令也是固定套路先建虚拟环境再装依赖cd RehabAssessment python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt uvicorn app.main:app --reload --port 8000这里--reload只在开发期用生产部署去掉如果科室电脑上有别的服务占着 8000 端口改--port 8001就行。前端页面如果要让局域网里的平板访问启动时加--host 0.0.0.0否则默认只监听本机病房里的平板打不开。3.3 表结构怎么建patient 与 assessment 的核心字段数据库结构是这个项目最值得抄作业的部分。康复评估数据的特点是一个患者多次评估每次评估有多个条目条目数随量表变化。所以用三张表最稳患者表存基本信息评估会话表存“某天做的某次量表”评估明细表存“这次评估里每个条目的分”from sqlalchemy import Column, String, Integer, Float from sqlalchemy.orm import declarative_base Base declarative_base() class Patient(Base): __tablename__ patients patient_id Column(String, primary_keyTrue) # 外部病历号不乱生成 name Column(String, nullableFalse) age Column(Integer) diagnosis Column(String) class AssessmentSession(Base): __tablename__ assessment_sessions id Column(Integer, primary_keyTrue, autoincrementTrue) patient_id Column(String, indexTrue) scale_id Column(String) assessment_date Column(String) # 存 ISO 格式 2024-03-01 total Column(Float) level Column(String) class AssessmentDetail(Base): __tablename__ assessment_details id Column(Integer, primary_keyTrue, autoincrementTrue) session_id Column(Integer, indexTrue) item_id Column(String) score Column(Integer)这里我故意把assessment_date存成字符串而不是日期类型原因是 SQLite 的日期处理能力弱跨库迁移到 MySQL 时 ISO 字符串也能直接用。只要统一存YYYY-MM-DD格式字典序就是时间序。AssessmentDetail用长表而不是宽表新加量表不需要改表结构这是整套设计里最值得坚持的一个决定。4. 数据集怎么用从 CSV 读到康复趋势曲线源码包里的数据集一般不是给你训练模型的而是给你当“演示数据”和“校验样本”用的。常见是三份 CSV患者档案、评估记录、关节采样数据。评估记录如果存的是长表格式那刚好和数据库里的AssessmentDetail表对应上可以无缝导入。但直接拿 CSV 画趋势图之前有三个地方必须处理干净编码、日期和字段合并。4.1 用 pandas 读取患者档案与评估记录字段语义与合并第一步先把数据读进来。我这里统一用encodingutf-8-sig原因后面第 5 章会细说现在先养成习惯import pandas as pd patients pd.read_csv(data/patients.csv, encodingutf-8-sig) records pd.read_csv(data/assessment_records.csv, encodingutf-8-sig) # patients: patient_id, name, age, sex, diagnosis, onset_date # records: patient_id, scale_id, assessment_date, item_id, score print(patients.head()) print(records.head())这段代码的重点不在读取本身而在理解 records 是一行长表一行代表“某患者某次评估中某一条目的得分”。为什么要用长表因为康复评估的量表条目数量不一样FMA 有 50 项Barthel 只有 10 项长表不需要为了短量表补一堆空列。但画趋势图时长表要按评估会话汇总成总分session_total ( records.groupby([patient_id, scale_id, assessment_date]) .agg(total_score(score, sum)) .reset_index() ) print(session_total.sort_values(assessment_date).head())这个groupby里的三个字段缺一不可。如果漏了scale_idFMA 和 Barthel 的分数会混在一起求和趋势线就没有意义如果漏了assessment_date同一天多次评估会被叠成一行丢失随访粒度。4.2 数据校验评分越界、编码错误与日期排序数据集不是官方发布的干净数据来源多半是手工整理或历史 Excel常出的问题有三个日期是字符串、分数越界、同一个条目分数集合混乱。我用一段代码同时查这三类问题# 1) 日期统一成 datetime 格式防止字符串排序出错 records[assessment_date] pd.to_datetime( records[assessment_date], format%Y-%m-%d ) # 2) 按条目检查每个分值是否落在合法集合里 problem_items [] for item_id, values in records.groupby(item_id)[score].apply(set).items(): if values - {0, 1, 2}: # 以 FMA 为例只允许 0/1/2 problem_items.append((item_id, sorted(values))) print(可疑条目:, problem_items)这里values - {0, 1, 2}是集合差集意思是“这个条目里出现了不在 0/1/2 范围内的分值”。如果数据集混入了一个 5 分说明这个量表模板版本或分值集合定义有问题。实际项目里我会把第二个检查升级成模板驱动从scale_templates里读出每个条目的allowed_scores再和数据的实际分值集合比对能反向揪出模板配置错误效果比人工看强得多。4.3 画一条恢复曲线横轴用评估日期而不是训练次数数据校验通过后画趋势曲线是最直观的汇报方式。康复科的医生关心的是“患者从入院到现在功能恢复的轨迹”横轴应该是评估日期不是自然日期也不是训练次数因为评估日期才是真正记录功能状态的时刻import matplotlib.pyplot as plt def plot_recovery(records, patient_id, scale_idfma): df records[ (records[patient_id] patient_id) (records[scale_id] scale_id) ].sort_values(assessment_date) total df.groupby(assessment_date)[score].sum() fig, ax plt.subplots(figsize(8, 4)) ax.plot(total.index, total.values, markero) ax.axhline(96, ls--, colorgreen, lw1, labelFMA 轻度障碍参考线 96) ax.set_title(f{patient_id} 的 {scale_id} 康复趋势) ax.set_xlabel(评估日期) ax.set_ylabel(总分) ax.legend() return fig画图代码里最该注意的是axhline(96)这条参考线。它让读者一眼看到患者有没有越过“轻度障碍”这条临床意义线比光秃秃一条折线有用得多。另一个注意点是排序sort_values(assessment_date)必须在groupby之前否则折线会按原始数据顺序乱跳。注意同一患者同一天不能有两条同量表评估记录。如果数据集里有重复趋势线上会出现竖直的跳变线。画图前先drop_duplicates按患者、量表、日期去重。5. 康复评估系统避坑5 条能省半周的踩坑记录这个章节写的每一条都是我在做康复类系统时真实遇到过、并且能在源码上直接定位到原因的坑。每条按“现象、原因、解决”来写照着排查能省下不少时间。5.1 量表版本混用FMA 运动功能版和 126 分总表不是一回事现象报告里显示“上肢得分 60/66下肢得分 25/34”总分 85 被判定为“明显运动障碍”但治疗师说这个患者恢复得还可以。原因FMA 有多个版本网上流传的“完整版 FMA 量表”是 126 分总表包含感觉、关节活动度和疼痛评估而运动功能版只有 100 分。代码里的分级阈值套用了 126 分的标准导致 100 分版的分数被低估。解决模板里加一个version字段报告上直接展示“FMA 运动功能版UE 33 项 LE 17 项总分 100”。上线前找两三份真实评估记录做对照验证确认阈值和总分来自同一个版本。5.2 分值集合没校验Barthel 洗澡项只有 0/5 分现象录入端把“洗澡”选成 10 分系统算出总分 105超过 100 后分级逻辑错乱。原因网上流传的 Barthel 表把“洗澡”写成 0/5/10但原版只有 0/5。我没在模板里按条目限定分值集合只设了个总分上限。解决模板定义阶段给每个条目写清楚allowed_scores前端生成选项、后端校验都依赖这份配置。非法分值直接拒绝不要让系统“宽容”地算下去。5.3 从 Excel 导出的 CSV 列名带着 \ufeff现象pd.read_csv(patients.csv)之后打印列名第一列显示\ufeffpatient_id和数据库字段patient_idmerge 时一直 KeyError。原因Excel 的“另存为 CSVUTF-8”带 BOM 头pandas 按无 BOM 解析时把\ufeff并进第一个列名。解决所有手工来源的 CSV 统一用encodingutf-8-sig读取写回时加indexFalse避免行号列混进去。这个习惯要从第一天就养成不要等数据洗了三遍才发现。5.4 同一患者的多条记录同时进训练集和验证集现象用评估记录训练模型预测“下次能否达到轻度障碍”交叉验证 AUC 有 0.9部署后掉到 0.7。原因康复随访数据天然按患者聚集同一个人的多次评估高度相关。随机切分把同一个患者的记录同时分进训练集和验证集模型实际上在“记忆这个人”而不是学习康复规律。解决验证集必须按patient_id分组用GroupKFold或GroupShuffleSplit切分并且保证同一天的多次记录不能被分到两个 fold 里。做完切分后检查一下每个 fold 里的患者 ID 是否有重叠这一步不能省。5.5 前端页面直接调 API 被 CORS 拦住现象浏览器打开static/index.htmlfetchhttp://localhost:8000/api/assessments时控制台报 “has been blocked by CORS policy”。原因FastAPI 默认不添加CORSMiddleware浏览器不允许跨源请求。前端文件从本地文件系统打开时源跟 API 服务不是一个源。解决入口文件里加上app.add_middleware(CORSMiddleware, allow_origins[*], allow_methods[*])。开发期放开没问题生产部署时把*换成科室内网的实际地址。6. 进阶技巧把关节活动度ROM接进评估结论如果这个源码包只有量表评估它的价值到上一章就结束了。但康复评估真正有区分度的地方是把“主观量表”和“客观运动数据”结合起来。我这里教一个能立刻接进去的技巧从关节角度采样数据里计算活动度ROM并输出一句评估结论。假设data/joint_samples.csv里有这样的字段patient_id、timestamp、motionflex/ext、angle_deg。要算一次肩关节前屈的活动范围代码可以这样写import pandas as pd from scipy.signal import savgol_filter df pd.read_csv(data/joint_samples.csv, encodingutf-8-sig) subject df[df[patient_id] P001].sort_values(timestamp) # 只取屈曲动作段过度到伸展段会引入无效区间 flex subject[subject[motion] flex].copy() # 滤波去抖动直接用 max-min 会把动作起止时的抖动量算进去 angle savgol_filter(flex[angle_deg], window_length11, polyorder2) rom angle.max() - angle.min() print(f肩关节前屈 ROM: {rom:.1f}°)这里savgol_filter的window_length11是经验值适合 50Hz 左右的采样率采样率高到 100Hz 时可以调到 15。如果源码包没带 scipy用移动平均也可以但 Savitzky-Golay 滤波对峰值保持更友好。ROM 算出来之后可以按参考范围出结论比如肩前屈正常参考是 180° 左右def rom_level(rom): if rom 160: return 基本正常 if rom 120: return 轻度受限 if rom 60: return 中度受限 return 显著受限这个分级只是示例实际阈值要按被测关节和患者人群调。接入系统时rom()作为services/rom.py里的一个函数评估报告里同时显示“FMA 得分 肩前屈 ROM”比单独一个量表分数更能说明问题。验证这个方法是否可靠我习惯造两组模拟数据自测一组是健康的 30° 到 180° 屈曲一组是受限的 30° 到 90° 屈曲喂给同一个compute_rom()确认健康组被判“基本正常”、受限组被判“显著受限”。如果连这两条理想曲线都分不出来那真实数据里的噪声只会更惨。这几年做康复类系统我最深的教训是康复评估的难点不在界面也不在算法而在“让治疗师认可你算出来的数”。每一个分数、每一条参考线都要能追溯到量表原文或科室共识否则做出来的系统只会躺在 U 盘里。把模板配置化、把校验前置、把数据集里的日期和编码盯死这套最小系统就能成为治疗师每天真正在用的工具。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站