☰
中风风险数据分析与可视化:从Django到深度学习的完整实践
2026/10/14 5:42:04 网站建设 项目流程

有朋友拿着“中风人群的数据分析与可视化”这个题目来找我的时候,第一反应是这题不太好做。数据在哪、分析用什么方法、Django到底要写到什么程度、深度学习模型放进去是加分还是添乱——这些问题在动手前全是未知数。等项目真正做完,我反而觉得这类题目比很多看着高大上的选题更值得选:它强迫你把数据分析、算法模型和Web系统完整地串成一条链路,而不是停留在“训练一个模型然后保存结果”的演示层面。

这篇文章就把我梳理这个项目的完整思路、技术选型、踩坑记录和答辩要注意的点都展开说说。适合正在做医工交叉方向毕设、或者想拿一个“有业务含义、有技术深度、有界面呈现”的综合型项目的人来说,应该能帮你少走不少弯路。

1. 中风风险数据:这种综合型选题到底在做什么

1.1 医学背景:中风数据不是一堆数字,而是一组风险信号

中风在医学上叫脑卒中,是脑血管被堵住或者破裂导致的脑组织损伤。它不是一个单一原因造成的疾病,而是多种危险因素共同作用的结果。公开的脑卒中风险数据集里常见的字段,比如年龄、高血压、心脏病史、平均血糖、BMI、吸烟状态,每一列背后都有明确的医学解释。

我做这个项目的时候,第一件事不是急着调模型,而是先把这些字段和医学含义对了一遍。高血压是中风最重要的可控危险因素之一,长期血压高会损伤血管壁;血糖异常说明可能存在糖尿病或者糖耐量异常,这类人群血管病变风险显著上升;肥胖(BMI偏高)往往伴随着血脂异常和血压升高。年龄则是不可干预因素,数据里60岁以上人群的中风记录比例明显更高,这一点和医学常识完全吻合。

理解了字段含义之后,分析的思路就清楚了:不是让模型去“诊断”某个人有没有中风,而是通过历史统计数据找到“什么样的人群风险更高”。整条分析链路围绕风险分层、因素关联、指标交互来展开,这样后期做可视化和Web展示时,每张图表都能讲出业务含义,而不是干巴巴地堆数据。

1.2 技术骨架:一个“有区分度”的毕设通常分四层

纯做管理系统、纯做算法调参这类题目,答辩时很容易被一轮轮追问打穿。我的经验是,一个有区分度的综合分析型项目,至少要在项目里完整铺开四层技术栈:

  • 数据层:用Pandas做数据读取、清洗、缺失值处理、类别编码。这一层决定了后续模型的天花板。
  • 可视分析层:用Matplotlib、Seaborn做探索性分析,刻画不同因素与中风的关系。
  • 模型层:用Scikit-learn跑基线模型,再用Keras搭建深度学习网络做分类预测。
  • 系统层:用Django把数据统计结果和模型推理包装成Web接口,前端用ECharts呈现可视化大屏。

这个选题的聪明之处在于,每一层都有可以量化的产出。数据层有清洗前后的对比,可视分析层有图表,模型层有AUC和混淆矩阵,系统层有在线风险预测交互。答辩的时候不管老师从哪一层切入,你都有的讲。反过来,如果只做其中一两层,往往会出现技术栈太单薄或者展示不落地的问题。

2. 数据清洗与特征工程:模型性能从数据准备开始

2.1 数据集的字段与医学含义对照

项目使用的是公开研究用的脱敏脑卒中风险数据,大概包含五千多条记录,每条记录对应一个匿名个体的健康指标和是否中风的标签。字段不算多,但每个字段都需要明确它的类型和处理方式。我整理了一个对照表,后面做特征工程时一直用这个表来统一思路:

字段类型医学含义处理方式
gender类别性别二值编码
age数值年龄归一化 + 分箱
hypertension二值是否高血压0/1映射
heart_disease二值是否曾患心脏病0/1映射
ever_married二值婚姻状态0/1映射
work_type类别工作类型OneHot
Residence_type二值城市/农村0/1映射
avg_glucose_level数值平均血糖水平归一化
bmi数值身体质量指数填充 + 归一化
smoking_status类别吸烟状态OneHot
stroke二值是否发生中风(标签)目标变量

这份字段清单基本就是把中风危险因素的常见维度都覆盖了一遍。字段不多反而有好处:特征工程的工作量可控,而且每个特征都能讲到医学依据,答辩时不用心虚。

2.2 缺失值、类别编码和不平衡样本的处理链路

数据质量直接决定模型效果,这个项目里最典型的问题有三个。

第一个是缺失值。bmi字段有少量缺失,在数据集中占比大概一两成左右,直接删除会丢掉很多样本,所以我选择用中位数填充。为什么不用均值?因为BMI的分布有一些右偏,极端值会把均值拉高,中位数对偏态分布更稳健。吸烟状态字段缺失比例更高,直接用众数填充会丢掉“缺失本身也是一种信息”的含义,更稳的做法是单独增加一个“unknown”类别,保留缺失情况供模型学习。

第二个是类别编码。二值变量像高血压、心脏病史,直接映射成0和1就行。但工作类型、吸烟状态这类多类别变量,不能贪图省事用LabelEncoder,因为它会给类别强行加上一个不存在的顺序关系。这里应该用OneHot编码,把每个类别展开成独立的0/1特征向量。

第三个是类别不平衡。数据集中中风样本占比只有5%左右,如果直接拿原始数据训练,模型会倾向于把所有样本都预测为“未中风”,因为这样准确率也能到95%。项目里我同时用了两种手段:一是SMOTE过采样,合成部分少数类样本;二是在训练时给少数类设置更高的class_weight,让模型对错分中风样本的惩罚更大。两个手段可以配合使用,后面调模型时会看到召回率有明显变化。

2.3 特征衍生与相关性洞察

原始特征经过清洗编码之后,还可以做一步特征衍生,让后续可视化和模型都能吃得更透。

我对年龄做了分箱处理:小于40岁算青年组,40到59岁算中年组,60岁及以上算老年组。对BMI也按标准划分了偏瘦、正常、超重、肥胖四个区间。分箱的好处有两个:一是连续变量变成有业务含义的区间,做图表更直观;二是能构造交叉统计,比如看“高血压×年龄组”下中风比例的变化,这种二维交叉在展示阶段特别出效果。

相关性分析我看了两个层面。特征层面的热力图显示,age和avg_glucose_level与中风标签的相关性相对更强;业务层面的分组统计则发现,血糖值偏高的群体中风比例显著上升,老年组的风险也明显高于中青年。这些结论都可以作为答辩时的业务洞察,整条项目逻辑从数据到结论是自洽的。

3. 深度学习建模:先跑基线,再来调优

3.1 为什么先跑逻辑回归和随机森林,再上神经网络

不少人拿到数据集就急着搭深度学习模型,实际上这是一个容易踩坑的操作。五千条样本、十几个特征,这个规模下深度学习并不是天生占优的。我建议先跑两个基线模型——逻辑回归和随机森林,拿到一组数字作为参照,再判断神经网络带来的增益值不值得。

我当时跑出来的代表性结果大概是这样的:

模型准确率精确率召回率AUC
逻辑回归0.940.320.180.78
随机森林0.970.450.240.80
多层感知机MLP0.960.520.470.85

注意看,准确率上随机森林甚至更高,但一旦把视角转到召回率和AUC,MLP的优势就显现出来了。医学筛查场景最怕漏掉真正的高风险人群,模型哪怕把一些正常人误判为高风险,后续还有医生复核;但如果是真正的高风险病人被漏过,问题就大了。所以在这个业务场景下,召回率比准确率重要得多。

选择MLP的理由不是因为它“叫深度学习”,而是因为它能自动学习特征之间的交叉组合。逻辑回归只能做线性加权,随机森林能捕捉一些非线性但解释性较弱;MLP通过隐藏层的非线性激活,可以把年龄和血糖、血糖和BMI之间的交互模式一起学出来。配合类别平衡策略后,召回率的提升是三个模型里最明显的。

3.2 网络结构与训练策略设计

既然是深度学习算法项目,网络结构这部分得讲清楚。项目里用的是经典多层感知机结构,三层隐藏层逐步压缩特征维度:

from tensorflow.keras import Sequential from tensorflow.keras.layers import Dense, Dropout from tensorflow.keras.optimizers import Adam model = Sequential([ Dense(64, activation='relu', input_shape=(input_dim,)), Dropout(0.3), Dense(32, activation='relu'), Dropout(0.3), Dense(16, activation='relu'), Dense(1, activation='sigmoid') ]) model.compile( optimizer=Adam(learning_rate=1e-3), loss='binary_crossentropy', metrics=['AUC', 'Recall'] )

为什么是三层而不是更深的网络?因为样本量就摆在那,层数过多很容易把训练集背下来,导致验证集表现差。64→32→16的逐层压缩思路,是把输入特征慢慢抽象成更高层的表示。每个隐藏层后面接了Dropout,比例设为0.3,训练时随机丢掉一部分神经元的输出,等于在隐式地做模型集成,这是MLP防止过拟合最有效的手段之一。

训练时设置早停机制,监控验证集损失,连续十个epoch没有下降就停止训练,最大训练轮次限制在100。这样既能保证模型充分收敛,又不会在平台期白白浪费时间。最终保存的是一个H5格式的模型文件,后面Django系统直接加载这个文件做在线推理。

3.3 评估指标:这个项目的“分数”应该怎么打

我见过很多同学在论文里写“模型准确率达到95%”,这其实是个危险表述。在正负样本比接近1:19的数据集里,准确率是一个高度误导性的指标——模型把所有样本都预测为阴性,准确率照样有95%。答辩老师大概率会抓住这一点追问,到时候就很难收场。

所以我展示模型效果时只用三个核心指标:召回率、AUC、F1分数。召回率说明“真实中风的人里模型抓到了多少”,AUC说明“模型把高风险和低风险人群区分开的能力”,F1分数则综合考虑了精确率和召回率。测试集上的混淆矩阵也一定要画出来,真实中风样本有多少被正确识别、多少被漏掉,一眼就能看清。我的测试集里,原始不平衡训练时模型几乎只抓到少数几个中风样本;加入SMOTE和class_weight之后,召回率从个位数百分比升到了40%以上,AUC稳定在0.85左右。这个提升过程本身就是很好的答辩素材。

还要检查过拟合的痕迹:训练集的AUC如果明显高于验证集,就说明模型背题了,这时优先调大Dropout比例或者减少隐藏层宽度,而不是继续加epoch。

4. Django落地:把模型和数据变成看得见的系统

4.1 工程项目结构与模块划分

建模只是项目的一半,另一半是把模型结果和统计图表装进Django系统里。项目结构我按职责拆成了四个模块,避免所有逻辑堆在views.py里变成一坨不可维护的代码:

stroke_project/ ├── manage.py ├── config/ │ ├── settings.py │ └── urls.py ├── analysis/ │ ├── views.py │ ├── urls.py │ ├── services.py │ └── data_processor.py ├── prediction/ │ ├── views.py │ ├── urls.py │ └── model_api.py ├── static/ │ ├── css/ │ └── js/ ├── templates/ │ ├── dashboard.html │ └── predict.html └── models/ └── stroke_model.h5

analysis模块负责所有统计图表的数据接口,比如总体概览、筛选条件过滤后的统计数据;prediction模块专门负责模型加载和预测推理;data_processor.py里存放特征编码器和归一化器,它们也要独立保存成pickle文件,和模型一起供Django调用。这样拆分之后,每一层都是独立的,改前端不影响模型,换模型也不影响统计接口。

4.2 可视化大屏设计:每张图表都要有落点

前端可视化我选了ECharts而不是后端Matplotlib生成图片,原因是ECharts天然支持交互,筛选联动效果好,而且中文资料多,遇到问题容易找到解决方案。本地探索阶段可以用Seaborn和Plotly,但最终呈现在Django页面上的,一定是浏览器端的交互图表。

大屏上我放了八个模块,每张图都对应一个可讲述的分析结论:

  • 总览卡片:总样本量、中风人数、患病率、性别分布
  • 年龄分箱中风率柱状图:展示风险随年龄上升的趋势
  • 性别×高血压分组堆叠柱状图:展示不同性别、不同血压状态下中风率的差异
  • 血糖区间与中风率折线图:展示血糖升高带来的风险变化
  • BMI分组箱线图:观察中风险和非中风人群的体型差异
  • 特征重要性排序条形图:让模型的黑盒结论可解释
  • ROC曲线和AUC:直观呈现模型区分能力
  • 混淆矩阵热力图:回应“模型到底抓到了多少人”这个问题

页面逻辑是页面加载时先请求一个总览接口,拿到数据后渲染各图表。筛选器部分放了年龄段、性别、高血压三个下拉框,用户选择条件后,前端带着参数重新请求筛选统计接口,返回结果后图表联动更新。这种交互虽然实现起来不复杂,但演示效果非常加分,答辩现场操作一段就能让老师直观感受到系统是“活”的。

4.3 模型推理接口:实时风险评估怎么做

在线预测是另一个核心亮点,用户在前端表单输入自己的年龄、血糖、BMI、是否高血压等信息,后端调用已训练好的模型实时返回中风风险概率。

模型加载这块有个细节值得单独说:不要在每次请求里都调用load_model。模型文件加载一次需要几百毫秒,如果每个用户请求都加载一次,并发稍微一高接口就废了。正确做法是模块启动时加载一次并缓存到全局变量:

from tensorflow.keras.models import load_model _model = None def get_model(): global _model if _model is None: _model = load_model('models/stroke_model.h5') return _model

预测接口设计成POST请求,接收前端传来的JSON数据。后端拿到参数后,先做格式校验和类型转换,然后复用训练时保存的编码器和归一化器,把输入数据转换成和训练阶段完全一致的特征向量,再调用模型的predict方法输出概率值。返回结果包含三个字段:风险概率、风险等级、提示语。

{ "risk_probability": 0.73, "risk_level": "中高风险", "suggestion": "建议结合体检报告进一步评估" }

这里最容易翻车的地方是特征顺序。训练时特征向量的排列顺序必须和生产环境推理时完全一致,否则模型拿到的是一堆错位的数据,结果毫无意义。我的解决方式是把编码器和归一化器对象连同特征列顺序列表一起保存为pickle文件,推理时先读取列顺序,再按顺序构造输入向量,从根上避免手动拼字段出错。

5. 答辩与复盘:容易被追问的几个关键点

5.1 数据来源和合规性怎么说

答辩时“数据哪来的”基本是必问题。正确的说法是:项目使用的是公开的脱敏研究数据集,字段中不包含姓名、身份证号、联系方式等任何个人身份信息,每一条记录都是匿名的健康指标和标签,整个项目仅用于学术研究和系统演示,不涉及真实患者的隐私数据,也不构成任何医学建议。

要特别注意不要随口说数据来自某家医院、某个科室。如果是公开数据就说公开数据,如果有合作才说合作,医疗数据的授权链条在答辩现场经不起追问,编造数据来源是高风险操作。

5.2 模型可解释性:黑盒模型怎么向医学人员交代

导师可能会追问一个很实际的问题:神经网络是黑盒,你怎么向临床医生解释这个模型为什么给出高风险判断?这个问题如果答不上来,前面模型做得再好也会打折扣。

我做了两个层面的工作。第一是特征重要性排序,利用置换重要性或者集成树模型的特征重要性,给每个特征打一个分数,排序结果清晰显示年龄、平均血糖、BMI排在前三,这和医学常识保持一致。第二是SHAP值分析,挑出预测为高风险的样本,逐样本展示每个特征对其风险概率的贡献方向,正面特质和反面特质一目了然。

这两项工作做完之后,黑盒说服力的问题基本就化解了:模型内部虽然复杂,但输入和输出之间的整体逻辑仍然可以用特征归因来解释。

5.3 项目的局限性和扩展方向

主动讲局限性和扩展方向,是答辩加分的姿态。我总结了这个项目可以坦诚接受的几个不足:

  • 样本量只有几千条,模型泛化能力有限,换一个地区的数据集可能效果会波动。
  • 数据是横截面快照,缺乏时间维度,比如一个人的血压在半年内的变化趋势没有纳入。
  • 特征不完整,缺少血脂、家族史、既往中风史等对评估也很重要的指标。

扩展方向可以从三个角度切入:一是接入更大规模的多中心数据集,让模型覆盖更多人群;二是把静态预测升级为时序预测,比如记录多次随访数据后预测未来一段时间的中风风险;三是在Django系统中加入更多交互维度,例如让用户调整不同指标的取值,观察风险概率的实时变化趋势。这几个方向既符合医学研究趋势,也体现你对项目延展性有思考。

做这类医工交叉项目,我最大的体会是:最有价值的不是跑出来多高的AUC,而是把整条链路从头到尾走通了。数据清洗时踩过的编码顺序坑、训练模型时反复调类别权重的过程、Django里模型加载缓存的优化,这些环节单独拎出来都不算难,但组合在一起,就是一个完整的工程思维训练。最后再分享一条实操建议:如果时间紧张,先跑通最小闭环——数据读进来、模型训练完、页面能打开——然后再去叠加筛选联动、SHAP解释这些花活。不少项目死在“想一次做完整”上,分阶段交付反而更容易收尾。

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

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

立即咨询