医疗预测模型实战:从Jupyter Notebook到可复用脚本的避坑指南
简介本资源为基于Kaggle中风数据集构建的机器学习预测项目面向希望掌握医疗数据分类建模全流程的学习者与数据科学从业者。项目以Jupyter Notebook为主要载体围绕性别、年龄、疾病史与吸烟状况等特征预测个体是否可能发生中风并尝试构建F1分数与AUC均达到理想水平的分类模型。压缩包共11个文件约4.36MB包含3个ipynb实验笔记、1个csv原始数据、1个pkl模型文件、1个py部署脚本、1个sh环境脚本、1个Procfile及说明文档等覆盖数据探索、特征处理、模型训练与Heroku部署等环节。已有740人学习下载。读者可据此获得完整的赛题式建模方案、可复用的数据清洗与可视化思路、模型持久化与Web部署参考以及从原始数据到预测服务的目录组织范例适合作为医疗预测类项目的实战模板。1. 从一份 Jupyter Notebook 说起stroke-prediction-model 能跑出什么如果你手头正好有一份stroke-prediction-model的源码包打开一看大概率是一个.ipynb文件里面塞满了数据清洗、特征工程、模型训练和评估的单元格。这类资源在医疗数据挖掘圈子里很常见但真正能直接跑通、且结果能拿来做二次分析的并不多。我拿到这份 Notebook 的第一反应是它到底用的是真实临床数据集还是模拟数据特征列里有没有泄漏标签模型评估是只看准确率还是兼顾了召回率这些问题不搞清楚跑出来的 AUC 再高也是自嗨。这份资源的核心价值在于它提供了一个完整的二分类预测流程——从原始表格数据到中风风险概率输出。适合两类人一是刚接触医疗预测建模、想找一个能跑通的端到端案例的开发者二是需要快速验证特征工程思路、不想从零搭框架的从业者。前提是你得接受它可能用的是公开模拟数据集而不是某家医院的真实病历。下面我按实际拆包顺序把这份 Notebook 里的关键环节和容易翻车的地方逐一过一遍。2. 环境与数据加载Notebook 跑通前的三个硬检查2.1 依赖版本与内核选择stroke-prediction-model这类 Notebook 通常依赖pandas、numpy、scikit-learn、matplotlib、seaborn这几个库。我一般不会直接pip install最新版而是先看 Notebook 里有没有import语句旁边的版本注释。如果没有就按下面这个组合来兼容性最稳# 创建独立环境避免和系统包冲突 python -m venv venv_stroke source venv_stroke/bin/activate # Windows 用 venv_stroke\Scripts\activate # 安装核心依赖版本区间经过实测 pip install pandas1.5.3 numpy1.23.5 scikit-learn1.2.2 pip install matplotlib3.7.1 seaborn0.12.2 jupyter1.0.0逻辑说明scikit-learn1.2.x 对OneHotEncoder和ColumnTransformer的 API 已经稳定不会出现 1.0 之前sparse参数报错的问题。pandas1.5.x 读取 CSV 时对缺失值的推断更保守能减少DtypeWarning干扰。参数上唯一要注意的是jupyter包本身只是启动器真正跑代码的是内核所以装完记得python -m ipykernel install --user --namevenv_stroke否则 Notebook 里选不到这个环境。2.2 数据文件路径与编码陷阱Notebook 里读数据的单元格通常长这样import pandas as pd df pd.read_csv(healthcare-dataset-stroke-data.csv) df.head()但实际解压后CSV 文件可能藏在data/子目录里或者文件名带版本后缀。我习惯先跑一段路径探测import os for root, dirs, files in os.walk(.): for f in files: if f.endswith(.csv): print(os.path.join(root, f))逻辑说明这段代码会列出当前目录下所有 CSV 的完整路径避免FileNotFoundError。参数上os.walk默认自上而下遍历如果目录层级很深可以加topdownTrue并手动剪枝。另一个常见坑是编码——如果 CSV 里有中文列名或特殊符号pd.read_csv默认utf-8可能报UnicodeDecodeError换成encodinglatin-1或encodinggbk试一下。我遇到过一份数据用utf-8-sig才能去掉 BOM 头否则第一列列名会带\ufeff。2.3 标签列与 ID 列的识别加载完数据后第一件事是确认哪一列是预测目标。stroke-prediction-model里目标列通常叫stroke取值 0 或 1。但有些版本会把id列也读进来这列必须删掉否则模型会学到“ID 越大越容易中风”这种荒谬规律。我一般用下面这段做快速体检# 查看列名、类型、缺失值 print(df.info()) print(df[stroke].value_counts(normalizeTrue)) # 删除无意义的 ID 列如果存在 if id in df.columns: df df.drop(id, axis1)逻辑说明value_counts(normalizeTrue)输出的是类别比例如果正样本占比低于 5%说明数据严重不平衡后续评估不能只看准确率。参数上drop的axis1表示按列删除inplace默认 False所以必须重新赋值给df。这一步做完数据才算真正准备好进入特征工程。3. 特征工程与预处理把分类变量塞进模型之前3.1 数值特征与分类特征的拆分原始数据里通常混着数值列如age、avg_glucose_level、bmi和分类列如gender、ever_married、work_type、smoking_status。直接扔进sklearn的LogisticRegression会报错因为分类列是字符串。我一般先手动分列num_cols [age, avg_glucose_level, bmi] cat_cols [gender, ever_married, work_type, Residence_type, smoking_status]逻辑说明bmi列经常有缺失值后面要单独处理。age虽然是数值但实际业务里常做分箱不过 Notebook 里如果没做先保留连续值也能跑。参数上num_cols和cat_cols的划分依据是df.dtypesobject类型的基本都是分类列但要注意有些列虽然存的是数字实际是类别编码比如hypertension只有 0/1这种可以当数值也可以当分类我一般当数值处理减少维度。3.2 缺失值填充中位数还是均值bmi列的缺失比例通常在 3% 到 5% 之间。Notebook 里常见做法是用中位数填充from sklearn.impute import SimpleImputer num_imputer SimpleImputer(strategymedian) df[num_cols] num_imputer.fit_transform(df[num_cols])逻辑说明SimpleImputer的strategy参数可选mean、median、most_frequent、constant。对bmi这种可能有偏态分布的列中位数比均值更稳健不会被极端肥胖值拉偏。参数上fit_transform一步完成计算填充值和应用填充但要注意如果后面有测试集必须用fit过的imputer去transform不能重新fit否则数据泄漏。我见过有人把训练集和测试集合并后一起填充交叉验证分数虚高上线就翻车。3.3 独热编码与列转换器分类列不能直接丢给模型需要独热编码。sklearn的ColumnTransformer可以把数值和分类处理串起来from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline preprocessor ColumnTransformer( transformers[ (num, SimpleImputer(strategymedian), num_cols), (cat, OneHotEncoder(handle_unknownignore), cat_cols) ]) X df.drop(stroke, axis1) y df[stroke] X_processed preprocessor.fit_transform(X)逻辑说明ColumnTransformer的transformers参数是一个列表每个元素是(名称, 转换器, 列名列表)。OneHotEncoder的handle_unknownignore很关键——如果测试集里出现了训练集没见过的类别比如新的一种work_type不加这个参数会直接报错加了之后会输出全零向量。参数上sparse默认 True输出稀疏矩阵如果后面用LogisticRegression没问题但用RandomForest最好设sparseFalse或.toarray()转稠密。这一步做完X_processed的列数会从原来的十几列膨胀到几十列属于正常现象。4. 模型训练与评估准确率之外的三个关键指标4.1 逻辑回归基线为什么先跑它stroke-prediction-model里通常第一个模型是LogisticRegression。不是因为它效果最好而是因为它可解释性强、训练快、能给出概率输出。我一般这样搭from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score X_train, X_test, y_train, y_test train_test_split( X_processed, y, test_size0.2, random_state42, stratifyy) clf LogisticRegression(class_weightbalanced, max_iter1000) clf.fit(X_train, y_train) y_pred clf.predict(X_test) y_prob clf.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, y_prob))逻辑说明stratifyy保证训练集和测试集的正负样本比例一致避免随机切分导致某一集里正样本极少。class_weightbalanced会自动按类别频率反比加权缓解不平衡问题。max_iter1000是因为默认 100 次迭代在特征多的时候可能不收敛会弹ConvergenceWarning。参数上predict_proba返回两列第一列是负类概率第二列是正类概率所以取[:, 1]。评估时classification_report看recall和f1-scoreroc_auc_score看整体排序能力。如果 AUC 在 0.8 左右说明特征有一定区分度如果只有 0.5 出头基本等于随机猜。4.2 随机森林与特征重要性第二个模型通常是RandomForestClassifier用来对比非线性模型是否更好同时输出特征重要性from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators200, max_depth10, class_weightbalanced, random_state42, n_jobs-1) rf.fit(X_train, y_train) import pandas as pd feat_names preprocessor.get_feature_names_out() importances pd.Series(rf.feature_importances_, indexfeat_names) print(importances.sort_values(ascendingFalse).head(10))逻辑说明n_estimators200是树的数量太少方差大太多训练慢200 是常见折中。max_depth10限制树深防止过拟合。n_jobs-1用满所有 CPU 核心。get_feature_names_out()能拿到ColumnTransformer处理后的列名比如num__age、cat__gender_Male这样特征重要性才有可读性。参数上class_weightbalanced在随机森林里同样有效但注意它和bootstrap采样结合时少数类样本会被重复抽到可能加剧过拟合所以max_depth要适当调小。4.3 阈值调整与混淆矩阵默认分类阈值是 0.5但在医疗场景里漏判一个中风高风险患者的代价远大于误判。所以我会手动调阈值from sklearn.metrics import confusion_matrix threshold 0.3 y_pred_adjusted (y_prob threshold).astype(int) print(confusion_matrix(y_test, y_pred_adjusted))逻辑说明y_prob是正类概率 threshold得到布尔数组.astype(int)转成 0/1。阈值从 0.5 降到 0.3召回率会上升精确率会下降。具体降到多少要看业务能接受多少误报。参数上confusion_matrix输出[[TN, FP], [FN, TP]]重点看FN漏判和FP误判。我一般会画一条precision_recall_curve来选阈值但 Notebook 里如果没画手动试几个值也能找到感觉。5. 避坑与排查这份 Notebook 里最容易翻车的五个地方5.1 现象AUC 高得离谱接近 0.99原因数据泄漏。常见于在train_test_split之前做了全局的缺失值填充或独热编码导致测试集信息渗入训练集。解决把所有预处理步骤塞进Pipeline只在训练集上fit测试集只transform。检查ColumnTransformer是否在切分之后才fit。5.2 现象运行到OneHotEncoder报ValueError: Found unknown categories原因测试集里出现了训练集没有的类别且handle_unknown默认是error。解决初始化时显式写OneHotEncoder(handle_unknownignore)。如果已经跑了重新fit整个ColumnTransformer。5.3 现象LogisticRegression不收敛弹ConvergenceWarning原因特征未标准化数值列量纲差异大比如age是 0-100avg_glucose_level是 50-300梯度下降震荡。解决在ColumnTransformer的数值分支里加StandardScaler()或者把max_iter调到 2000 以上。我一般两者都做。5.4 现象bmi列填充后分布畸变模型对肥胖样本预测偏保守原因用均值填充导致分布向中间聚集丢失了极端值信息。解决改用中位数填充或者加一列bmi_missing标记是否缺失让模型自己学缺失模式。参数上SimpleImputer的add_indicatorTrue可以自动加缺失指示列。5.5 现象Notebook 重启后结果对不上随机种子失效原因train_test_split和模型初始化都设了random_state但RandomForest的n_jobs在多核并行时可能引入非确定性。解决把n_jobs设为 1 做最终复现或者接受微小波动。另外检查numpy和scikit-learn的随机数生成器版本是否一致跨版本结果可能不同。6. 进阶技巧把 Notebook 变成可复用的预测脚本跑通 Notebook 只是第一步真正要用起来得把它变成能接收新数据、输出概率的脚本。我一般会抽出一个predict_stroke_risk函数把预处理和模型打包import joblib # 训练完后保存 pipeline 和模型 joblib.dump(preprocessor, preprocessor.pkl) joblib.dump(clf, stroke_model.pkl) # 新数据预测 def predict_stroke_risk(new_df): preprocessor joblib.load(preprocessor.pkl) model joblib.load(stroke_model.pkl) X_new preprocessor.transform(new_df) prob model.predict_proba(X_new)[:, 1] return prob逻辑说明joblib比pickle更适合保存numpy数组和sklearn对象序列化速度快、文件小。参数上transform只做转换不做拟合保证新数据走和训练集完全相同的处理路径。注意new_df的列名和顺序必须和训练时一致否则ColumnTransformer会按位置匹配出错。我习惯在函数入口加一句new_df new_df[original_columns]强制对齐。验证方法上我会留出 20% 数据做一次盲测然后手动构造几条边界样本比如 80 岁、高血压、高血糖、吸烟看输出概率是否显著高于基线再构造 20 岁、无病史、不吸烟看概率是否接近 0。如果边界样本的排序不符合医学常识说明特征工程或模型选择有问题得回头查。从那以后我每次拿到一份新的 Notebook 资源都强制先跑一遍数据体检和泄漏检查再谈模型效果。希望帮到你。本文还有配套的精品资源点击获取