☰
Python康复评估系统源码:Barthel指数评分闭环与Django实现
2026/10/20 15:06:37 网站建设 项目流程

简介:这是一套基于Python开发的康复评估系统完整项目,包含源码与配套数据集,面向计算机、人工智能、自动化等专业在校学生与开发者,既适合作为毕业设计、课程设计的直接参考,也可用于项目初期立项演示或Python学习进阶。项目以BVH动作捕捉数据为核心,配合CSV数据表与Python脚本实现康复指标的读取、计算与分析,目录内另附IPython Notebook分析笔记和说明文档,帮助使用者快速梳理研究流程。资源包共157个文件,其中CSV数据文件70个、BVH动作文件68个、Python源文件11个,另含.ipynb、.pyc、.md等辅助类型,压缩包整体约16.18MB。目前已有260人学习下载。借助完整动作数据和已测试的代码,读者可快速理解系统运行逻辑并构建演示环境,并可在此基础上扩展新的评估算法或适配自有数据,尤其适合需要快速搭建康复评估原型的高校学生与开发者。

1. 康复评估系统源码:是「能跑完评估闭环」的项目,不是患者表 CRUD

很多课程设计和毕业设计里的“康复评估系统”,说白了就是患者表的增删改查,页面不少,数据却停在“录入”这一步。答辩时被问到“评估逻辑在哪”,就卡住了。这套基于 Python 的康复评估系统源码不像表面那么简单:它把康复科最常用的 Barthel 指数做成了可执行的评分代码,患者建档、逐项打分、自动分级、趋势图报告是一条完整链路,压缩包里还带了一份可直接导入的模拟患者评估数据集。解压后装依赖、迁移数据库、导入 CSV,半小时内能跑起来。适合需要提交一个能演示闭环毕设/课设的在校生,也适合想借医疗业务系统骨架练手的 Python 开发者。

2. 康复评估的业务骨架:先理解评估流程,再碰代码

2.1 技术栈与目录结构:这套源码的每个文件是干什么的

一套完整的康复评估系统至少要解决三个问题:患者档案往哪存、量表得分怎么算、评估结果怎么展示。这套源码的常见做法是用 Python 的 Django 框架做 Web 层,SQLite 做本地数据库,Bootstrap 搭页面,matplotlib 出统计图。选型逻辑很直白:Django 自带 ORM 和后台管理,患者 CRUD 几乎不用手写;SQLite 零配置,课程设计演示环境不需要单独装数据库服务;matplotlib 生成的图片直接嵌进报告页面,不依赖前端图表库。

解压后你会看到类似下面的结构:

rehab_system/ ├── manage.py # Django 管理入口,启动、迁移、建后台账号都靠它 ├── requirements.txt # 依赖清单,pip 一键安装 ├── dataset/ │ └── patient_assessment.csv # 模拟患者评估数据,初始化演示环境用 ├── rehab/ # 项目主应用 │ ├── settings.py # 数据库、静态文件、语言时区配置 │ ├── urls.py # 路由,决定每个 URL 进哪个视图 │ ├── models.py # 患者、评估记录等数据模型 │ └── views.py # 视图函数,处理页面请求和表单提交 ├── algorithms/ # 算法目录,量表评分和分级边界全在这里 │ ├── barthel.py # Barthel 指数评分与分级 │ ├── fugl_meyer.py # FMA 运动功能评分框架 │ └── visualizer.py # 趋势图生成,输出 base64 图片 ├── templates/ # Django 模板 │ ├── base.html # 公共布局 │ ├── index.html # 首页患者列表 │ ├── assess_form.html # 评估表单页 │ └── report.html # 评估报告页 └── static/ # CSS/JS,Bootstrap 资源放这里

这个结构最大的特点是「Web 层」和「算法层」分离。algorithms 目录是整套项目的核心资产:后续新增量表只需要在这个目录里加一个评分函数,视图层基本不动。这是这套资源后期扩展的底子,也是改造成自己毕设时最值得保留的设计。

2.2 数据库模型:患者、评估记录、量表得分怎么串成一条线

先看 models.py,这是整套系统的数据地基:

from django.db import models class Patient(models.Model): name = models.CharField(max_length=64, verbose_name="姓名") gender = models.SmallIntegerField(choices=((0, "男"), (1, "女")), default=0, verbose_name="性别") age = models.IntegerField(verbose_name="年龄") diagnosis = models.CharField(max_length=255, blank=True, verbose_name="诊断") created_at = models.DateTimeField(auto_now_add=True, verbose_name="建档时间") class Meta: db_table = "rehab_patient" class AssessmentRecord(models.Model): patient = models.ForeignKey(Patient, on_delete=models.CASCADE, related_name="records") assess_type = models.CharField(max_length=32, verbose_name="量表类型") total_score = models.FloatField(default=0, verbose_name="总分") level = models.CharField(max_length=16, blank=True, verbose_name="分级结果") detail = models.JSONField(default=dict, verbose_name="逐项得分明细") assess_date = models.DateField(auto_now_add=True, verbose_name="评估日期") class Meta: db_table = "rehab_assessment"

这里的关键设计是 detail 字段用 JSON 保存逐项得分。为什么不单独建明细表?因为量表类型会越来越多,今天有 Barthel,明天加 FMA,评估项的数量和结构各不相同,一张一张建明细表,模型就僵死了。用 JSONField 存明细,总分单独用 FloatField 存,既保证列表页能按总分排序筛选,又保留了后续二次分析的原始数据。

一个患者对应多条评估记录,外键设置在 AssessmentRecord 上,patient 关联的 records 就是这名患者的历次评估史。没有这个外键关系,报告页就展示不了多时间点的对比,康复训练前后的效果变化就无从谈起了。

2.3 评估主流程:从录入到报告输出的四步链路

页面行为通常是这样的:首页列出患者,点击评估进入量表选择;逐个录入评估项得分并提交;视图调用评分函数算出总分和分级;跳转报告页并生成趋势图。

请求路径视图函数作用
/index患者列表与最近评估摘要
/assess/<patient_id>assess_form渲染量表录入表单
/submit/<patient_id>submit_assessment提交得分并执行评分计算
/report/<record_id>report报告页与趋势图

对应到代码,是 views.py 里的两个动作:

from django.shortcuts import render, redirect, get_object_or_404 from .models import Patient, AssessmentRecord from algorithms.barthel import calculate_barthel def assess_form(request, patient_id, assess_type="barthel"): patient = get_object_or_404(Patient, pk=patient_id) return render(request, "assess_form.html", { "patient": patient, "assess_type": assess_type, "error": None, })

这个视图只负责把患者信息带到表单页。表单页只做录入,不做计算,是这套流程里很值得借鉴的一个习惯:计算全部下沉到算法层,页面重复刷新也不会因为中间状态算出脏数据。

提交时的处理在另一个函数里:

def submit_assessment(request, patient_id): patient = get_object_or_404(Patient, pk=patient_id) if request.method == "POST": # 剔除 Django 自动注入的 csrfmiddlewaretoken,避免污染明细数据 form_data = {k: v for k, v in request.POST.items() if not k.startswith("csrf")} try: total, level = calculate_barthel(form_data) except ValueError as e: # 带错误信息回到表单页,不能让用户看到 500 页面 return render(request, "assess_form.html", { "patient": patient, "error": str(e), }) record = AssessmentRecord.objects.create( patient=patient, assess_type="barthel", total_score=total, level=level, detail=form_data, ) return redirect("rehab:report", record_id=record.pk) return redirect("rehab:index")

注意 try/except 那段:评分函数发现缺项或越界直接抛 ValueError,页面回到表单并显示原因。医疗评估数据马虎不得,缺一项算出错误分级,在真实科室里是要出问题的。虽然这是课程设计,但从这里建立数据责任意识,答辩时是个能讲的点。

提交成功后跳转到 report 视图,把这条 AssessmentRecord 读出来,再让 visualizer 生成趋势图。管理系统到这里就是完整闭环了。

2.4 本地运行:从解压到跑起来的完整命令序列

拿到压缩包后的第一件事是建虚拟环境。直接装进系统 Python 容易和其他项目互相污染依赖,后面改需求时特别痛苦。

cd rehab_system python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python manage.py migrate # 建表 python manage.py createsuperuser # 创建管理员账号,按提示输入用户名和密码 python manage.py runserver # 启动开发服务器

四条命令里最容易出问题的是 migrate。如果报no such table,先执行python manage.py makemigrations再 migrate;如果报table already exists,属于迁移记录和数据库状态不一致,第 4 章的避坑记录专门讲这个场景。

数据集导入我习惯用 shell 命令:

python manage.py shell -c " from dataset.loader import load_csv load_csv('dataset/patient_assessment.csv') "

load_csv 内部用 utf-8-sig 打开文件,逐行创建 Patient,再按行的量表列生成 AssessmentRecord。CSV 文件如果带了 BOM 头,普通 utf-8 会把 BOM 读成不可见字符拼进第一个字段名,utf-8-sig 能直接消掉,这个编码坑下面专门讲。

跑完这两步,打开 http://127.0.0.1:8000 用管理员账号登录,首页就是完整的患者列表和评估入口。此时系统已经可以前后点通了,接下来深入算法层。

3. Barthel 评分算法:量表规则、边界值与报告可视化

3.1 Barthel 指数评分函数:把纸面规则结构化

Barthel 指数是康复科日常生活活动能力(ADL)评估最常用的量表,满分 100 分,涉及进食、洗澡、修饰、穿衣、大便控制、小便控制、如厕、床椅转移、行走、上下楼梯共 10 项。每项得分是离散的,要么 0,要么 5,要么 10 或 15,分档固定。这种结构特别适合在代码里定义成一张映射表。

def calculate_barthel(items: dict) -> tuple: """ 计算 Barthel 指数总分与分级。 items 是前端表单提交的评估项字典,键为评估项编码,值为得分。 返回 (总分, 分级字符串)。 """ # 量表定义:评估项编码 -> 单项满分 barthel_full_score = { "feeding": 10, # 进食 "bathing": 5, # 洗澡 "grooming": 5, # 修饰 "dressing": 10, # 穿衣 "bowel": 10, # 大便控制 "bladder": 10, # 小便控制 "toileting": 10, # 如厕 "transfer": 15, # 床椅转移 "mobility": 15, # 平地行走 "stairs": 10, # 上下楼梯 } total = 0.0 for key, full in barthel_full_score.items(): value = items.get(key) if value is None or value == "": raise ValueError(f"缺少评估项: {key}") score = float(value) if score < 0 or score > full: raise ValueError(f"{key} 得分超出范围 0-{full}: {score}") total += score return total, assess_level(total)

这段代码的核心是把「量表定义」和「计算逻辑」分离。字典同时承担两项职责:一是校验单项得分边界,二是提供累加上限。数据在进入求和之前就被卡了一道,后续分级拿不到超范围的总分。如果某项没填,直接抛 ValueError 而不是当 0 分处理,这样缺项可以立刻暴露。

可能有同学问:为什么不用 Django 表单的 validators?我一般更倾向于在算法层先做校验,因为评分函数可能在多个场景被调用,导入历史数据、接口对接都会绕开表单直接进函数。表单校验只能护住页面入口,函数内部校验才能护住所有入口。这个习惯建议保留,后面扩展 FMA 量表时同样适用。

3.2 分级判定与边界值:阈值切错就换档

算出总分之后,接下来是分级。康复评估的分级在不同医院略有差异,一个常见的参考切分是:0-20 完全依赖,21-40 重度依赖,41-60 中度依赖,61-90 轻度依赖,91-99 基本独立,100 完全独立。

# constants.py 里单独维护阈值表,改档位不需要动函数 BARTHEL_LEVELS = [ (20, "完全依赖"), (40, "重度依赖"), (60, "中度依赖"), (90, "轻度依赖"), (99, "基本独立"), (100, "完全独立"), ] def assess_level(total: float) -> str: total = round(total, 2) for limit, level_name in BARTHEL_LEVELS: if total <= limit: return level_name return "完全独立"

这段看着简单,但边界值最容易翻车。total 是浮点数,20、40、60、90 这些整数边界一旦带上了浮点误差,比如 39.999999,用total < 40判断就会被错误拉低一档。这里统一先round(total, 2),再用<=明确包含边界值,语义是「总分落在半开区间 (上个阈值, 当前阈值]」,自洽且稳定。阈值表单独放 constants.py,评审想调档位,改一张表就行,不用翻函数。

另一个边界问题出在特殊患者:昏迷患者无法完成行走、上下楼梯,很多初版代码会直接传 0。这本身没问题,但如果传「无法评估」或者空字符串,float(value)直接抛异常。我一般会在评估表单里为这种情况设置「不适用」选项,算法层把它映射成 0 分,同时在 detail JSON 里保留原始标记,报告页能区分「实际得 0」和「不适用」。这两者在临床上含义不同,丢了会导致后期数据分析失真,这个细节答辩时讲了,评审一般会认可。

3.3 评估报告与趋势图:让数据能「被看见」

单次评估总分只是一个点,康复治疗的价值体现在多次评估的变化曲线上。visualizer.py 里常见做法是画趋势折线:横轴是评估日期,纵轴是总分,同一患者的多次评估串成一条线,一眼看出治疗有没有效果。

import matplotlib.pyplot as plt from io import BytesIO import base64 def draw_trend(records) -> str: """根据患者历次评估记录生成趋势图,返回 base64 图片字符串。""" dates = [str(r.assess_date) for r in records] scores = [r.total_score for r in records] plt.figure(figsize=(8, 4)) plt.plot(dates, scores, marker="o", linewidth=2) plt.ylim(0, 110) # 留出 100 分以上的可视空间 plt.ylabel("Barthel 总分") plt.xlabel("评估日期") plt.title("Barthel 指数变化趋势") buf = BytesIO() plt.savefig(buf, format="png", dpi=120) plt.close() data = base64.b64encode(buf.getvalue()).decode() return f"data:image/png;base64,{data}"

这段生成的不是磁盘图片文件,而是 base64 字符串,直接塞进模板的 img src。部署时不用处理静态文件路径,也不会污染 media 目录。dpi=120 是为了让报告打印时清晰度够用,dpi=80 会明显发虚。画完图马上plt.close()释放当前 figure,否则大量患者评估时 figure 对象堆积在内存里,页面会越来越卡。

这里有个诚实提示:在 Windows 环境第一次跑,标题里的中文很容易渲染成方块,这是 matplotlib 字体回退的老毛病。完整的处理办法在第 4 章踩坑记录里。想快速验证「图能出来」,跑这段就够了;想端出去演示,请先照 4.3 的步骤给 matplotlib 配好中文字体。

4. 避坑指南:复现这套康复评估系统最容易踩的五个坑

4.1 数据库迁移报「表已存在」:重复 migrate 的现场还原

现象:执行python manage.py migrate时报OperationalError: table "rehab_patient" already exists,或者反过来报no such table: rehab_patient。

原因:前一种是迁移记录和数据库实际状态不一致。常见场景是从别处拷了旧的 db.sqlite3 文件进来,再跑 migrate,Django 发现迁移记录里已执行过,但表结构和模型对不上;也有同学直接 runserver 让 Django 自动建表,后来又改用 migrate 流程,两边各建了一次。后一种 no such table 则是迁移文件缺失:models.py 写了模型,但应用目录下没有生成 0001_initial.py。

解决:如果数据库里没有必须保留的数据,直接删库重迁最省心:

# 1. 备份当前数据库 cp db.sqlite3 db.sqlite3.bak # 2. 删除该应用的旧迁移文件,保留 __init__.py rm -f rehab/migrations/000*.py # 3. 从当前模型重新生成迁移文件 python manage.py makemigrations rehab # 4. 应用到全新数据库 python manage.py migrate

如果 db.sqlite3 里有真实数据不能丢,可以python manage.py migrate --fake把迁移记录标记为已完成,但代价是表结构可能和模型不一致。课程设计阶段数据随时能重新导入,直接删库重迁,不要恋战,这是最快后退路。

4.2 CSV 导入乱码或字段错位:文件没坏,是编码变了

现象:用 Excel 打开 dataset 里的 patient_assessment.csv,编辑后另存,再导入系统,控制台报UnicodeDecodeError,或者患者名变成「锟斤拷」一类乱码;有时候导入成功但第一个字段名多了一个看不见的字符,导致字段对不上。

原因:Excel 另存 CSV 时默认用系统 ANSI 编码,Windows 下通常是 GBK,和代码里 open 用的 utf-8 不匹配,读取直接崩。UTF-8 文件带 BOM 时,普通 utf-8 会把 BOM 读成\ufeff,拼进第一个字段名,DictReader 的 key 就对不上模型字段了。

解决:读取时统一用 utf-8-sig,并且可以在导入函数里做一次表头校验:

import csv def load_csv(path: str, required_columns: list): with open(path, "r", encoding="utf-8-sig") as f: reader = csv.DictReader(f) if not required_columns.issubset(reader.fieldnames): raise ValueError(f"CSV 缺少必要列,当前列: {reader.fieldnames}") for row in reader: # 按模型字段构建 Patient,评估项字段按“量表_评估项”格式动态读取 ...

utf-8-sig 既能处理 BOM,又兼容纯 UTF-8 文件;对 GBK 文件仍然无解。所以另一个更稳的做法是引导使用者把数据集当只读资源,不要随手用 Excel 另存,改动通过页面录入完成。做好这一条,比事后治乱码省心得多。

4.3 报告图里的中文全变方块:matplotlib 字体回退问题

现象:Web 页面中文显示正常,但趋势图的标题、坐标轴文字全是「□ □ □」。

原因:matplotlib 自带字体库不包含中文字形,默认字体 DejaVu Sans 渲染不出汉字,就回退成方块。

解决:在生成图表前动态注册系统中的中文字体:

import os import matplotlib.pyplot as plt from matplotlib import font_manager def setup_chinese_font(): """按操作系统选择可用的中文字体路径并注册到 matplotlib。""" candidates = [ "C:/Windows/Fonts/simhei.ttf", # Windows 黑体 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", # 多数 Linux "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", # 文泉驿备选 ] font_path = next((p for p in candidates if os.path.exists(p)), None) if font_path is None: raise RuntimeError("未找到中文字体,请先安装 Noto Sans CJK 或文泉驿") font_manager.fontManager.addfont(font_path) font_name = font_manager.FontProperties(fname=font_path).get_name() plt.rcParams["font.sans-serif"] = [font_name, "sans-serif"] plt.rcParams["axes.unicode_minus"] = False

把这段放到 visualizer.py 模块顶部,每次绘图自动生效。axes.unicode_minus要顺手设成 False,否则坐标轴出现负数时,负号也会被渲染成方块。某些精简版 Linux 发行版根本没装中文字体,FontProperties 不报错但渲染出来还是方块,这种情况需要用系统的包管理器安装 fonts-noto-cjk,装完重启服务再验证。

提示:字体路径因系统发行版不同有差异,最稳妥的做法是优先尝试前两个候选路径,不存在再继续往下找。不要硬编码单一路径,换台机器就废了。

4.4 总分 40 判成「中度依赖」:浮点精度和阈值判断的锅

现象:同一名患者手工算总分是 40.0,按量表规则应判重度依赖,系统却输出中度依赖;查数据库 total_score 又是 40.0,数据看着全对但结果不对。

原因:分级函数用的是elif total < 40,40.0 被挡在重度档之外。与此同时,部分评估项得分可能带小数(如 7.5、12.5),累加后 float 会变成 39.999999 或 40.000001,任何一个方向的小波动都会让结果在边界换档。

解决:两个点一起治。第一,评分函数返回前统一round(total, 2);第二,分级逻辑用阈值表遍历:

BARTHEL_LEVELS = [ (20, "完全依赖"), (40, "重度依赖"), (60, "中度依赖"), (90, "轻度依赖"), (99, "基本独立"), (100, "完全独立"), ] def assess_level(total: float) -> str: total = round(total, 2) for limit, level_name in BARTHEL_LEVELS: if total <= limit: return level_name return "完全独立" if total == 100 else "数据异常"

核心就是「边界值显式声明归属」。40 分归重度还是中度,每家医院定义可能不同,但代码里必须明确写出来,不能交给浮点误差去掷骰子。阈值表放在 constants.py,评审想改档位,只改数据不动逻辑。

4.5 患者列表页越点越慢:ORM 的 N+1 查询问题

现象:测试时只有 10 个患者,一切正常;导入 100 条评估记录后,首页加载要好几秒。第一反应以为是图表生成慢了,其实瓶颈在数据库查询次数。

原因:首页患者列表每行要显示「最近一次评估结果」,模板里如果写patient.records.last,Django ORM 会为每一行都执行一次新的 SELECT。10 个患者就是 10 次额外查询,患者和评估记录多了,查询次数成倍增长。这就是典型的 N+1 查询。

解决:视图里用 prefetch_related 把关联数据一次性查回内存:

def index(request): patients = list(Patient.objects.prefetch_related("records").all()) summary = [] for p in patients: # records 已在内存中,last() 不会再去数据库查 recent = p.records.last() summary.append({"patient": p, "recent": recent}) return render(request, "index.html", {"summary": summary})

模板里直接取 item.recent:

{% for item in summary %} <tr> <td>{{ item.patient.name }}</td> <td> {% if item.recent %} {{ item.recent.total_score }} 分 · {{ item.recent.level }} {% else %} 未评估 {% endif %} </td> </tr> {% endfor %}

注意:prefetch_related 对「反向外键」生效,select_related 对「正向外键」生效。如果顺着记录查患者(record.patient),用 select_related;从患者查评估史(patient.records),用 prefetch_related。两者可以链式使用。我建议在视图里提前把最近一条记录取好,模板只做展示,这样最稳,也不会因为模板里调用方法的方式不同而触发额外查询。

5. 把系统变成你的毕设亮点:扩展 FMA 量表与演示节奏

评审判定一个毕设是不是「抄的」,最常问的一个问题是:这套只能评 Barthel 吗?我想加一个别的量表,怎么加?这个问题其实就是突破口。扩展一个新量表,只需要在 algorithms 目录里写一个评分函数,再在视图里注册用量表类型分发。

以 FMA 上肢运动功能评分为例,共 33 个动作项,每项 0-2 分,总分 66,分数越高运动功能越好:

def calculate_fma(items: list) -> tuple: """ FMA 上肢运动功能:33 项,单项 0/1/2 分,总分 66。 items 是长度 33 的得分列表,按 Fugl-Meyer 标准动作顺序传入。 """ if len(items) != 33: raise ValueError(f"FMA 需要 33 项得分,当前收到 {len(items)} 项") total = 0.0 for idx, score in enumerate(items, start=1): s = float(score) if s not in (0.0, 1.0, 2.0): raise ValueError(f"第 {idx} 项得分必须为 0/1/2,收到 {s}") total += s return total, fma_level(total)

接入方式和 Barthel 完全一致:评估表单加一个量表类型下拉框,submit_assessment 里根据 assess_type 分发到不同评分函数。数据模型不用改,detail 是 JSONField,FMA 的 33 项得分以列表或字典形式塞进去都能存。

答辩演示不建议用系统自带的模拟数据一路点到底,评审看这种演示太多了。更好的节奏是:现场选一个患者,故意把「行走」从 10 分改成 5 分,提交后再做一次新评估,然后打开趋势图,指着下降的点说:系统能持续追踪功能变化,每次评估明细都存在 detail JSON 里,回溯时能指出是哪一项拖垮了总分。这一个动作,就把评估、存储、可视化三个模块全部串起来讲了。

把压缩包解压后按第 2 章的流程走一遍,这就是一个能讲清评估闭环的毕设底子;在此之上加量表、改阈值、换图表,都是加分动作。我印象里有一次演示前,一位同学全程照着脚本念,数据是提前导入好的,结果现场被问「为什么这条记录总分 40 分级是重度而不是中度」,他答不上来,因为没真正点过边界值。从那以后,我每次演示前都强制走一遍:先录一条满分,再录一条 40 分边界,最后录一条缺项分数,确认页面都能正确响应。这比背十页 PPT 都有用,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询