逻辑回归实战:胃癌预测分类与模型调优全流程
2026/9/12 16:14:57 网站建设 项目流程

简介:一项面向机器学习初学者的胃癌病人数据预测分类资料,基于逻辑回归算法完成疾病预测建模,适合正在学习分类模型、医疗数据分析或准备模式识别课程作业的读者。压缩包内含5个文件,主要类型为2个Python脚本、1个项目说明文档、1份docx报告和1个License文件,整体大小仅330KB。Python脚本中分别涉及逻辑回归原理演示与预测分类实现,文档和报告则辅助梳理建模思路与结果分析。目前已有1296人学习下载,验证了其作为入门参考的实用价值。读者可据此快速复现逻辑回归预测流程,理解数据加载、特征处理、模型训练与评估的完整步骤,并可在自己的课题中扩展使用。

1. 逻辑回归做胃癌预测分类:为什么医疗场景先选它

医疗数据建模有一个默认前提:解释性优先于绝对精度。胃癌病人的临床数据里,年龄、肿瘤标志物 CEA/CA19-9、病理分期、淋巴结转移数这些特征的量纲差异很大,样本量又往往卡在几百到几千这个区间,树模型和深度学习很容易在这个规模上过拟合,而逻辑回归在保证可解释性的同时,把每个特征对预测结果的贡献直接映射成对数几率。更关键的是,医疗场景要求我们回答“为什么这个病人被判为高风险”,而不是只给一个概率值。

这篇文章围绕一套可以直接复现的胃癌病人数据集,走完从数据清洗、特征编码、逻辑回归训练、预测分类到模型导出的完整链路。适合刚接触机器学习医疗落地的人,也适合想核对逻辑回归参数边界的老手,代码基于 scikit-learn 1.5.x 运行,数据文件放在脚本同级目录下即可执行。

2. 从病例数据到特征矩阵:胃癌数据清洗与编码的完整路径

2.1 读入病例数据并核对缺失值分布

逻辑回归对输入矩阵的要求很直接:数值型特征、无无穷值、缺失值要么删除要么填充。胃癌临床数据常见的问题是某些生化指标只在部分病人身上做了检测,比如 CA19-9 和 CEA 的缺失比例可能超过 30%。如果直接把缺失行删掉,样本量会急剧缩水,所以优先按列填充。

import pandas as pd df = pd.read_csv("gastric_cancer.csv") print(df.shape) print(df.isnull().sum()) print(df.dtypes)

这段代码先拿到数据规模、缺失值分布和每个字段的类型。注意df.dtypes这一行容易被忽略,如果某个数值列被读成了 object,后续建模会直接报错,常见原因是电子表格软件把数据导出成了带千分位逗号的文本,比如1,234会被解析成字符串。

对疾病分期的处理,我一般会用有序映射:I、II、III、IV 期转成 1、2、3、4。对于性别这类无顺序分类变量,用独热编码。年龄、肿瘤大小、CEA 数值这类连续变量保留原值,但逻辑回归对尺度敏感,后续需要标准化。缺失值策略可以统一如下表:

字段类型处理方式
年龄连续变量保留原值,参与标准化
性别二分类映射为 0/1,不做独热
病理分期有序分类1/2/3/4 数值映射
CEA连续变量中位数填充
CA19-9连续变量中位数填充
淋巴结转移数计数变量0 填充,并新增缺失标记列
生存状态标签映射为 0/1,死亡为 1

2.2 特征编码与训练集/测试集切分

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler df["gender_male"] = (df["gender"] == "M").astype(int) df["stage_num"] = df["stage"].map({"I": 1, "II": 2, "III": 3, "IV": 4}) df["lymph_missing"] = df["lymph_nodes"].isnull().astype(int) df["lymph_nodes"] = df["lymph_nodes"].fillna(0) features = ["age", "gender_male", "stage_num", "CEA", "CA19_9", "lymph_nodes", "lymph_missing"] X = df[features] y = (df["survival_status"] == "death").astype(int) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)

这里有几个关键点需要展开。第一,stratify=y保持训练集和测试集中“死亡/生存”的比例一致,避免切分后测试集只剩一种标签,这个问题在死亡比例只有 20% 的数据里非常常见。第二,标准化只 fit 训练集,再用同一组均值和方差 transform 测试集,如果对全部数据先标准化再切分,测试集的信息会泄漏到训练过程,导致评估结果虚高。第三,把生存状态转成二分类标签时,注意确认原始数据的编码方式,有的数据集用 0 表示存活、1 表示死亡,有的完全相反,可以先用df["survival_status"].value_counts()核对再映射。

lymph_missing这一列是容易被忽略的细节:淋巴结转移数缺失本身可能意味着没有做根治性清扫,这类病人的预后分布和有明确数值的病人不同,单纯用 0 填充会掩盖这种差异性,加一个缺失标记列可以让逻辑回归自己学习这个信号。

3. 训练逻辑回归代码:参数、正负样本不均衡与预测分类输出

3.1 用 scikit-learn 跑通最小逻辑回归代码

特征矩阵准备好之后,逻辑回归本身的训练代码非常短。核心是LogisticRegression这个类,我用max_iterclass_weight两个参数来应对胃癌数据里最常见的两个问题:特征标准化后仍然迭代不收敛,以及死亡样本占比过低。

from sklearn.linear_model import LogisticRegression model = LogisticRegression( max_iter=2000, C=1.0, class_weight="balanced", solver="lbfgs", random_state=42 ) model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) y_proba = model.predict_proba(X_test_scaled)[:, 1]

逻辑说明:solver="lbfgs"适合中小型数据集,默认lbfgs在特征数几百以内时收敛稳定性最好;max_iter=2000是因为标准化后虽然梯度下降更快,但部分特征存在共线性时会拖慢收敛。class_weight="balanced"会让模型按类别频率自动放大少数类的损失权重,胃癌数据集中死亡病例如果只占 25%,不设这个参数时模型会把所有样本都判成存活,因为整体准确率也能到 75%。

predict_proba返回的是二维数组,第一列是预测为 0 的概率,第二列是预测为 1 的概率,这里用[:, 1]取出“死亡风险”的概率值。注意逻辑回归默认的判定阈值是 0.5,但医疗场景里这个阈值几乎从来不是最优的,后面专门讲怎么调。

3.2 class_weight 与 C 值在胃癌数据上的调法

两个参数值得细抠:C是正则化强度的倒数,C越小正则化越强,系数越往 0 收缩;class_weight控制类别权重。在胃癌数据集上,我一般会让C在 0.01 到 10 之间按对数尺度搜索,而不是直接用默认值 1.0,因为临床特征之间常有多重共线性,比如肿瘤大小和病理分期高度相关,过大的C会把系数推到不稳定区间。

from sklearn.model_selection import GridSearchCV param_grid = { "C": [0.01, 0.1, 1, 10], "class_weight": ["balanced", None] } search = GridSearchCV( LogisticRegression(max_iter=2000, solver="lbfgs"), param_grid, scoring="roc_auc", cv=5 ) search.fit(X_train_scaled, y_train) print(search.best_params_) print(search.best_score_)

参数说明:scoring="roc_auc"是非常关键的取舍。如果这里用默认的accuracy,搜索出来的参数会偏向多数类,得到的模型在死亡风险高的病人上表现很差。cv=5表示五折交叉验证,每一折都用训练数据中的 4/5 训练、1/5 验证,最终分数是五折平均值,能比较稳定地反映参数组合的泛化能力。

一个常见的误用是直接用全量训练集做 GridSearchCV,然后又在同一份数据上评估最优模型的准确率,这会让指标虚高。正确做法是像上面这样,搜索过程完全不接触测试集,搜索结束后再用search.best_estimator_去预测X_test_scaled

3.3 输出预测分类结果并落盘

result = X_test.copy() result["y_true"] = y_test.values result["y_pred"] = y_pred result["risk_prob"] = y_proba result.to_csv("prediction_result.csv", index=False) print(result.groupby("y_pred").agg( sample_count=("y_true", "count"), death_ratio=("y_true", "mean") ))

这段代码把预测分类结果和真实标签写回 CSV,方便后续在 Excel 里核对哪些病人被错判。最后一行按预测类别分组,计算每个分组里的实际死亡比例,如果y_pred=0的那个组里death_ratio仍然很高,说明有相当多的高风险病人被漏掉了,这时候单看准确率是没有意义的,要回到召回率上做文章。

4. 评估胃癌预测分类效果:混淆矩阵、ROC 与最优阈值选择

4.1 混淆矩阵、召回率与准确率的取舍

胃癌筛查场景里,漏掉一个真正的高风险病人,代价远大于把一个小风险病人误判为高风险。准确率在正负样本不均衡时是守恒的指标,所以评估必须落到混淆矩阵的四个格子里。

from sklearn.metrics import confusion_matrix, classification_report cm = confusion_matrix(y_test, y_pred) print(cm) tn, fp, fn, tp = cm.ravel() sensitivity = tp / (tp + fn) # 召回率 specificity = tn / (tn + fp) # 特异度 print(f"Sensitivity: {sensitivity:.3f}") print(f"Specificity: {specificity:.3f}") print(classification_report(y_test, y_pred, target_names=["survival", "death"]))

cm.ravel()把混淆矩阵展平成[真阴性, 假阳性, 假阴性, 真阳性]四个值,这个顺序对应 sklearn 的行列排列,不要记错。敏感性也叫召回率,描述所有真实死亡病人中有多少被模型正确识别;特异度描述所有真实存活病人中有多少被正确放行。

如果这时候sensitivity只有 0.4,说明 60% 的高风险病人被漏掉了,说明默认的 0.5 阈值过于保守。我一般会直接去看 ROC 曲线,找到一个让敏感性和特异度都相对可接受的阈值,而不是继续调Cclass_weight,因为阈值调整是在不改变模型的情况下改变判别的截断点,成本和风险都更低。

4.2 ROC 曲线与最佳阈值搜索

ROC 曲线刻画的是不同阈值下真正例率和假正例率之间的 trade-off。AUC 是曲线下面积,它衡量的是模型把随机正样本排在随机负样本前面的概率,这个指标不依赖阈值,所以在调阈值之前先用 AUC 判断模型本身有没有区分度。

from sklearn.metrics import roc_curve, roc_auc_score fpr, tpr, thresholds = roc_curve(y_test, y_proba) auc = roc_auc_score(y_test, y_proba) print(f"AUC: {auc:.3f}") youden = tpr - fpr best_idx = youden.argmax() best_threshold = thresholds[best_idx] print(f"Best threshold by Youden's J: {best_threshold:.3f}") y_pred_adjusted = (y_proba >= best_threshold).astype(int)

Youden 指数tpr - fpr是找最佳阈值最常见的方式,它等价于同时最大化敏感性和特异度之和。搜索出来阈值可能不是 0.5,而是 0.31 或 0.68,这很正常。阈值低意味着模型更倾向于把病人判为高风险,适合筛查场景;阈值高则更保守,适合确诊后的治疗决策场景。阈值确定后,需要更新混淆矩阵并观察敏感性和特异度的变化,一般能看到敏感性显著提升,但特异性相应下降。

我习惯把 ROC 相关的数值一次性汇总到一个表里:

指标含义
AUC0.87模型区分度高
默认阈值 0.5敏感性 0.42高风险病人漏检严重
最佳阈值 0.31敏感性 0.78筛出更多高风险病人
最佳阈值 0.31特异性 0.82误报比例可接受

这里的数字只是展示格式,实际以你自己数据计算为准。调阈值之后y_pred_adjusted就是新的预测分类结果,后续的导出和落盘逻辑跟前面完全一致。

5. 模型落地:导出 pipeline 并做单条病例实时评分预测

5.1 用 joblib 导出完整的「预处理+模型」流水线

前面的代码把标准化和模型分开做了,这在实验阶段没问题,但上线时需要保证新来的单条病例经过完全相同的预处理。常见做法是用PipelineStandardScalerLogisticRegression串成一个对象,一次导出,避免线上推理时漏掉标准化或者搞错特征顺序。

import joblib from sklearn.pipeline import Pipeline pipeline = Pipeline([ ("scaler", StandardScaler()), ("clf", LogisticRegression(max_iter=2000, class_weight="balanced", C=0.1)) ]) pipeline.fit(X_train, y_train) joblib.dump(pipeline, "gastric_cancer_model.joblib")

这里必须注意一个顺序问题:前面实验里是scaler.fit_transform(X_train)之后再传给模型,而Pipeline会自动对X_train先做fit_transform、再做分类器fit,这保证测试时用pipeline.transform就不会有信息泄漏。导出用joblib.dump,它比 pickle 更适合包含 numpy 数组的大对象,加载时也只需要joblib.load

5.2 验证逻辑回归系数方向并检查特征对齐

模型上线前我总会做一次系数方向检查。对胃癌数据来说,年龄、病理分期、淋巴结转移数的系数应该为正,也就是数值越大死亡风险越高;如果某个已知的负面因素系数变成负数,基本可以断定特征编码或标签方向搞反了。

loaded = joblib.load("gastric_cancer_model.joblib") coef = loaded.named_steps["clf"].coef_[0] feature_names = X_train.columns for name, c in zip(feature_names, coef): print(f"{name}: {c:.4f}") patient = [[62, 1, 3, 18.5, 54.2, 3, 0]] risk = loaded.predict_proba(patient)[0][1] print(f"Predicted death risk: {risk:.3f}")

单条病例的推理要严格保持特征顺序,patient列表里的字段顺序必须和训练时的features完全一致,包括lymph_missing这个标记列。predict_proba返回的二维数组里,第二列才是目标类别的概率。如果后续要接实时评分,可以把loaded放进一个 Flask 接口里,每次请求构造同样的特征数组,返回risk和按阈值换算的分类标签。逻辑回归的计算量非常小,单条推理在毫秒级,即使没有 GPU 也能支撑高并发请求,这也是它至今仍是医疗实时评分主引擎的原因。

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

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

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

立即咨询