简介:这份资源是面向机器学习初学者与数据竞赛练习者的中风预测建模项目,基于Kaggle公开的中风数据集,通过性别、年龄、疾病史与吸烟状况等特征判断患者是否存在中风风险,适合用来练习分类建模、数据可视化与模型部署的完整流程。压缩包共11个文件,约4.36MB,包含3个Jupyter Notebook用于数据探索与建模分析,另有csv原始数据、pkl训练模型、py部署脚本、Procfile与sh启动脚本等,覆盖从数据处理到Heroku部署的环节,并附有说明文档与示意图。已有743人学习下载。项目以追求高F1分数与AUC为目标,读者可参考其中的特征筛选、类别不平衡处理与模型评估思路,并借助部署文件了解如何将预测模型封装为可访问的应用,适合作为课程设计或简历项目的实践参考。
1. 卒中预测建模:一份能直接跑通的 Jupyter Notebook 资源
拿到一份标题叫stroke-prediction-model的资源,第一反应往往是“又一个 Titanic 式的练手项目”。但真正拆开看,它解决的是一个很实际的问题:在结构化临床表格数据上,把“是否发生卒中”这个二分类任务从数据清洗一路做到模型评估,中间不跳过任何一个容易翻车的环节。这类资源对两类人最有价值——一类是想把机器学习流程走完整、但不想在环境配置和数据泄漏上反复卡壳的新手;另一类是手里有类似表格数据、想找一个可改可调的基线模板的从业者。整份资源以 Jupyter Notebook 为载体,意味着每一步的中间结果都可见、可改、可复现,而不是丢给你一个黑匣子脚本让你猜它干了什么。下面按“它是什么、怎么跑、坑在哪”的顺序拆。
2. 数据管道与特征工程:从原始表格到可训练矩阵
2.1 先看清数据长什么样,再决定怎么处理
卒中预测这类表格数据,典型结构是“一行一个样本、一列一个字段”,字段大致分三类:数值型(年龄、血糖、BMI)、类别型(性别、是否吸烟、是否高血压)、以及可能存在的缺失。打开 Notebook 的第一件事不是急着fit,而是把每列的 dtype、缺失率、取值分布打印出来。常见做法是先跑一段概览代码,把数值列和类别列分开,后面才能对症下药。
import pandas as pd import numpy as np df = pd.read_csv("stroke_data.csv") # 数值列与类别列分开,后续处理路径不同 num_cols = df.select_dtypes(include=[np.number]).columns.tolist() cat_cols = df.select_dtypes(include=["object"]).columns.tolist() # 缺失率概览:超过阈值的列要单独决策 missing = df.isnull().mean().sort_values(ascending=False) print(missing[missing > 0]) # 目标列分布,判断是否类别不平衡 print(df["stroke"].value_counts(normalize=True))这段代码的逻辑很直白:select_dtypes按类型切分字段,避免把类别列误当数值参与标准化;缺失率排序让你一眼看到哪些列需要插补、哪些列缺失太多干脆丢弃;目标列分布则决定后面要不要做重采样或调类别权重。参数上唯一需要留意的是缺失率阈值,我一般把 40% 作为分界线,超过就考虑整列删除,低于则插补。注意stroke这个目标列名要按实际数据替换,别照抄。
2.2 缺失值插补与类别编码的取舍
数值列常用中位数插补,类别列常用众数插补,这是最稳的默认方案。但这里有个容易被忽略的点:插补必须放在训练集上拟合、再应用到验证集,否则验证集的信息会泄漏进训练过程。Notebook 里如果图省事在全量数据上做fillna,后面的评估分数会虚高,这是血泪经验里最常见的一类翻车。
from sklearn.model_selection import train_test_split from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline X = df.drop(columns=["stroke"]) y = df["stroke"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) # 数值管道:中位数插补 + 标准化 num_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler()), ]) # 类别管道:众数插补 + 独热编码 cat_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="most_frequent")), ("encoder", OneHotEncoder(handle_unknown="ignore")), ]) preprocessor = ColumnTransformer([ ("num", num_pipe, num_cols), ("cat", cat_pipe, cat_cols), ])这里用Pipeline和ColumnTransformer把预处理封装起来,核心目的是让插补和编码只在fit时接触训练数据,transform时复用训练集的统计量。stratify=y保证训练集和测试集的类别比例一致,对不平衡数据尤其重要;handle_unknown="ignore"防止测试集出现训练时没见过的类别导致报错。参数上random_state=42只是固定切分,换成别的整数也行,但要保证全流程一致,否则复现时对不上号。
2.3 不平衡处理:别一上来就上 SMOTE
卒中数据通常正样本占比很低,直接训练会让模型倾向于全预测为负类,准确率看着高、召回率惨不忍睹。常见做法有三条路:调class_weight="balanced"、用 SMOTE 过采样、或者调分类阈值。我的建议是先试class_weight,因为它不改变数据分布、不引入合成样本,最不容易出问题;SMOTE 要放在训练集内部做,绝不能对测试集过采样,否则评估就失真了。
from imblearn.pipeline import Pipeline as ImbPipeline from imblearn.over_sampling import SMOTE # SMOTE 只在训练折内生效,测试集保持原始分布 model_pipe = ImbPipeline([ ("prep", preprocessor), ("smote", SMOTE(random_state=42)), ("clf", LogisticRegression(class_weight="balanced", max_iter=1000)), ])注意这里用的是imblearn的Pipeline而不是 sklearn 的,因为只有前者能保证重采样步骤在交叉验证的每一折内部执行。如果混用,SMOTE 会在切分前就把合成样本混进验证集,分数会好看得离谱,实际部署时原形毕露。max_iter=1000是给逻辑回归留足收敛空间,数据量大时默认值经常报收敛警告。
3. 模型训练与评估:把基线跑稳再谈调参
3.1 选逻辑回归还是树模型,先看你要什么
这份资源里如果同时给了逻辑回归和随机森林/梯度提升的对比,别急着认定树模型一定赢。逻辑回归的可解释性强,系数方向能直接对应风险因素,适合需要向非技术方解释的场景;树模型在特征交互复杂时表现更好,但调参成本高、容易过拟合。我的习惯是先跑逻辑回归当基线,拿到一个可解释的分数,再用树模型看能提升多少,提升不明显就保留简单模型。
from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score models = { "logreg": LogisticRegression(class_weight="balanced", max_iter=1000), "rf": RandomForestClassifier(class_weight="balanced", n_estimators=300, random_state=42), } for name, clf in models.items(): pipe = ImbPipeline([ ("prep", preprocessor), ("smote", SMOTE(random_state=42)), ("clf", clf), ]) scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring="roc_auc") print(name, scores.mean().round(4), scores.std().round(4))cross_val_score用 5 折交叉验证,scoring="roc_auc"而不是准确率,是因为不平衡数据下 AUC 更能反映排序能力。打印均值和标准差,标准差大说明模型对数据切分敏感,稳定性存疑。参数上n_estimators=300是随机森林的树数量,太少欠拟合、太多训练慢,300 是个折中值;cv=5在样本量不大时够用,样本上千可以加到 10。
3.2 评估指标别只看准确率
不平衡二分类的评估,准确率几乎没用。要看的至少包括召回率(漏诊多少)、精确率(误报多少)、F1 和 AUC。医疗场景下漏诊代价高,召回率往往优先,但也不能无限牺牲精确率,否则误报太多同样不可用。Notebook 里应该把混淆矩阵和分类报告都打出来,而不是只报一个数字。
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score best_pipe = ImbPipeline([ ("prep", preprocessor), ("smote", SMOTE(random_state=42)), ("clf", LogisticRegression(class_weight="balanced", max_iter=1000)), ]) best_pipe.fit(X_train, y_train) y_pred = best_pipe.predict(X_test) y_prob = best_pipe.predict_proba(X_test)[:, 1] print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, digits=3)) print("AUC:", round(roc_auc_score(y_test, y_prob), 4))predict_proba取第二列是因为正类在索引 1,这个细节写错会导致 AUC 算反。classification_report里重点看正类的 recall 和 precision,digits=3只是显示精度。如果召回率明显偏低,下一步不是换模型,而是调分类阈值——把默认 0.5 往下调,用召回换精确,具体阈值看业务能接受多少误报。
3.3 阈值调整:一个被低估的杠杆
很多人训完模型直接predict就完事,其实阈值是个免费的性能调节旋钮。把预测概率从高到低排序,逐个阈值算召回和精确,画一条曲线,选一个业务上可接受的点。这一步不需要重新训练,成本极低,但经常能把召回率拉上来好几个点。
import numpy as np from sklearn.metrics import precision_recall_curve precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob) # 找召回率不低于 0.8 的前提下精确率最高的阈值 mask = recalls[:-1] >= 0.8 best_idx = np.argmax(precisions[:-1][mask]) best_thr = thresholds[mask][best_idx] print("threshold:", round(best_thr, 3), "precision:", round(precisions[:-1][mask][best_idx], 3))precision_recall_curve返回的 thresholds 比 precisions/recalls 少一个元素,所以索引要错位处理,这是 sklearn 的一个固定行为,写错会报索引越界。recalls[:-1] >= 0.8是业务约束,你可以按实际需求改成 0.7 或 0.9。选出来的阈值要记下来,部署时用y_prob >= best_thr代替predict。
4. 避坑与排查:这几处翻车我见过太多次
4.1 数据泄漏:分数虚高的头号元凶
现象是交叉验证 AUC 高得离谱(0.95 以上),但换个数据集就崩。原因通常是在切分前做了插补、标准化或过采样,测试集信息渗进了训练。解决方法是把所有预处理塞进Pipeline,确保fit只碰训练折。检查手段很简单:故意把测试集标签打乱,如果分数还是很高,基本可以确定有泄漏。
4.2 类别编码顺序错乱
现象是训练时正常、预测时报“未见过的类别”。原因是OneHotEncoder没设handle_unknown="ignore",或者训练和预测用了两次独立的fit。解决方法是编码器只fit一次,保存下来供预测复用;用joblib把整个 pipeline 序列化,别只存模型权重。
4.3 随机种子不统一
现象是同一份代码跑两次结果差很多,复现不了。原因是train_test_split、模型初始化、SMOTE 各自用了不同的random_state或干脆没设。解决方法是在 Notebook 开头统一定义一个种子变量,所有涉及随机的地方都引用它,包括交叉验证的cv切分。
4.4 把 ID 列当特征喂进去
现象是模型在训练集上表现完美,测试集一塌糊涂。原因是数据里混了样本编号、就诊流水号这类唯一标识列,模型直接记住了 ID 和标签的对应关系。解决方法是在drop阶段显式排除 ID 类字段,并在特征列表里做一次人工核对,别全靠select_dtypes自动判断。
4.5 忽略特征量纲导致系数不可比
现象是逻辑回归系数大小悬殊,误以为某个特征特别重要。原因是数值列没标准化,年龄和血糖的量纲差了几个数量级。解决方法是在数值管道里加StandardScaler,标准化后的系数才能横向比较重要性。注意标准化同样要放在 Pipeline 里,不能提前对全量数据做。
5. 从 Notebook 到可复用脚本:固化流程的几个技巧
Notebook 适合探索,但不适合反复执行。把稳定下来的流程抽成函数或脚本,是这份资源能不能真正用起来的分水岭。我一般会做三件事:把预处理和模型封装成一个build_pipeline()函数,把评估逻辑封装成evaluate(pipe, X, y),再用joblib把训练好的 pipeline 落盘。这样下次换数据只需改读取路径,其余全复用。
import joblib def build_pipeline(num_cols, cat_cols, clf): num_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler()), ]) cat_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="most_frequent")), ("encoder", OneHotEncoder(handle_unknown="ignore")), ]) prep = ColumnTransformer([ ("num", num_pipe, num_cols), ("cat", cat_pipe, cat_cols), ]) return ImbPipeline([ ("prep", prep), ("smote", SMOTE(random_state=42)), ("clf", clf), ]) pipe = build_pipeline(num_cols, cat_cols, LogisticRegression(class_weight="balanced", max_iter=1000)) pipe.fit(X_train, y_train) joblib.dump(pipe, "stroke_pipeline.joblib")落盘的是整个 pipeline 而不是单独的模型,这样预测时插补、编码、重采样的参数全都带着,不会出现“训练时标准化了、预测时忘了”的低级错误。加载时joblib.load回来直接predict_proba即可。参数上唯一要盯的是handle_unknown="ignore"必须保留,否则线上遇到新类别直接抛异常。
验证方法我习惯用一个小脚本做冒烟测试:加载 pipeline,喂几条手工构造的样本,确认输出概率在 0 到 1 之间、且对明显高风险样本给出较高概率。这一步花不了几分钟,但能挡住大部分“模型存了但用不了”的问题。另外,特征重要性可以用逻辑回归的系数或树模型的feature_importances_打出来,和业务常识对一遍——如果模型认为“是否吸烟”不重要,那多半是编码或列名对错了,别急着信分数。
从那以后我每次交付这类表格建模资源,都强制走一遍“切分前不碰数据、pipeline 整体落盘、阈值单独调”这三步,少一步后面就得返工。希望帮到你。
本文还有配套的精品资源,点击获取