12 种机器学习模型 交叉验证 Pipeline 网格搜索调参用 Python 做胰腺癌生存状态预测这篇文章用一份 50000 行的胰腺癌预测数据集从数据理解、EDA 可视化、预处理 Pipeline、多模型横向比较、GridSearchCV 超参数调优、交叉验证到特征重要性完整走一遍医学表格数据的机器学习流程。本文不是医学诊断建议而是一次以第一性原理和费曼学习法来拆解机器学习建模的实战教程。本期摘要第一性原理预测不是“套模型”而是把病人的结构化信息 X 映射到生存状态 y。每一步都要服务于这个映射清洗字段、统一尺度、编码类别、控制数据泄漏、用交叉验证估计泛化能力。数据集50000 条样本24 个字段目标变量是Survival_Status。其中 0 类有 43578 条1 类有 6422 条类别明显不均衡。核心流程先 EDA 看数据脾气再用ColumnTransformer Pipeline把数值标准化、类别 One-Hot 和模型训练绑成一条可复现流水线。模型比较随机森林、梯度提升、SVM、KNN、决策树、朴素贝叶斯、逻辑回归、XGBoost、LightGBM、CatBoost、LDA、QDA 共 12 种模型横向比较。关键提醒很多模型 Accuracy 都在 0.873 左右看似不错但多数类基线本身就是 43578 / 50000 0.87156。医学预测里只看 Accuracy 很危险必须继续看 Recall、Precision、F1、ROC-AUC、PR-AUC 和混淆矩阵。引子把模型讲给没学过机器学习的人听费曼学习法要求我们把复杂概念讲到像日常语言一样直观。这里可以这样理解每个病人是一张表格化病历卡里面有年龄、性别、分期、症状、治疗方式、生活方式和医疗可及性等信息。模型要做的事是看过很多历史病历卡之后学习“哪些组合更可能对应某个生存状态”。第一性原理再往下拆机器学习不神秘它只做三件事。第一把真实世界变成数字第二在数字之间找稳定规律第三用没见过的新样本检验这个规律是否仍然成立。本文的 Pipeline、交叉验证、调参本质上都是为了让这三件事更可靠。第 0 部分环境与数据读取先导入依赖。这里既有传统机器学习模型也有 XGBoost、LightGBM、CatBoost 这类梯度提升框架。医学表格数据通常不是图像也不是长文本结构化表格模型仍然非常有竞争力。import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import warnings warnings.filterwarnings(ignore) from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.naive_bayes import GaussianNB from sklearn.linear_model import LogisticRegression from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis from xgboost import XGBClassifier from lightgbm import LGBMClassifier from catboost import CatBoostClassifier读取 CSV 后第一步不要急着训练模型而是先看形状、字段类型、缺失值和类别取值。你可以把这一步理解成“接诊问诊”先确认病历卡有没有漏项、字段是不是写错、每个变量到底表达什么。df pd.read_csv(pancreatic_cancer_prediction_sample.csv) print(df.shape) print(df.info()) print(df.describe()) print(df.isnull().sum()) categorical_cols df.select_dtypes(include[object]).columns for col in categorical_cols: print(f{col}: {df[col].unique()})本数据集共有50000 行、24 列。目标列Survival_Status是二分类标签0 类 43578 条1 类 6422 条。这个比例会直接影响后面对 Accuracy 的解读。第 1 部分先看数据不要先套模型年龄是医学预测里最常见、也最容易被模型重视的变量。先看年龄分布是为了确认数据是否集中在合理区间是否存在明显异常值。plt.figure(figsize(8, 5)) sns.histplot(df[Age], bins30, kdeTrue, colorblue) plt.title(Age Distribution of Patients) plt.xlabel(Age) plt.ylabel(Frequency) plt.show()图 1 年龄分布样本年龄以 60-70 岁附近最集中符合胰腺癌高发年龄段的常识背景。从图上看年龄主要集中在 60-70 岁附近。用费曼式说法如果一个班级里大多数学生都在同一个年龄段模型就很容易把“年龄段”当作重要线索。后面的特征重要性也印证了这一点。接着看诊断分期与生存时间。第一性原理上分期越晚肿瘤扩散越严重治疗窗口越窄生存时间通常越短。图像的价值就是让这个医学常识在数据里显形。plt.figure(figsize(8, 5)) sns.boxplot(xStage_at_Diagnosis, ySurvival_Time_Months, datadf, palettecoolwarm) plt.title(Survival Time Across Stages of Diagnosis) plt.xlabel(Stage at Diagnosis) plt.ylabel(Survival Time (Months)) plt.show()图 2 不同诊断分期下的生存时间Stage I 的生存时间明显更长Stage IV 更短。治疗方式分布也要看。不是因为柱状图高级而是因为样本数量会影响模型对某类治疗方式的学习。如果某个治疗方式样本很少模型对它的判断就更容易不稳定。sns.countplot(xTreatment_Type, datadf, paletteSet2) plt.title(Distribution of Treatment Types) plt.xlabel(Treatment Type) plt.ylabel(Count) plt.xticks(rotation45) plt.show()图 3 治疗方式分布化疗样本最多其次为放疗和手术。第 2 部分相关性、分布与异常值热力图不是为了好看而是为了回答一个问题变量之间有没有强相关如果两个变量几乎表达同一件事它们会让模型重复计分也会影响解释。plt.figure(figsize(10, 6)) sns.heatmap( df.select_dtypes(include[int64, float64]).corr(), annotTrue, cmapcoolwarm, linewidths0.5, ) plt.title(Correlation Heatmap of Numerical Features) plt.show()图 4 数值特征相关性热力图多数风险因素之间相关性较弱Age 与 Survival_Time_Months 是后续模型最敏感的连续变量。从相关性图看多数变量之间相关性较弱。这意味着模型需要从多变量组合里找信号而不是靠某两个变量的简单线性关系吃遍全场。下面的九宫格计数图把主要离散变量的样本比例放到一起看。第一性原理上分类模型学习的是“特征取值和标签之间的条件分布”。如果某些取值本来就占多数模型可能只是学会了样本比例而不是真正学会疾病风险。fig, axes plt.subplots(3, 3, figsize(18, 12)) sns.countplot(xGender, datadf, palettepastel, axaxes[0, 0]) sns.countplot(xSmoking_History, datadf, palettecoolwarm, axaxes[0, 1]) sns.countplot(xDiabetes, datadf, paletteSet2, axaxes[0, 2]) sns.countplot(xChronic_Pancreatitis, datadf, palettemagma, axaxes[1, 0]) sns.countplot(xStage_at_Diagnosis, datadf, paletteviridis, axaxes[1, 1]) sns.countplot(xTreatment_Type, datadf, paletteBlues, axaxes[1, 2]) sns.countplot(xSurvival_Status, datadf, palettehusl, axaxes[2, 0]) sns.countplot(xAlcohol_Consumption, datadf, paletteSet1, axaxes[2, 1]) sns.countplot(xPhysical_Activity_Level, datadf, palettecrest, axaxes[2, 2]) plt.tight_layout() plt.show()图 5 九个离散变量计数图先看类别比例才能判断模型学到的是信号还是样本占比。连续变量的 KDE、箱线图、小提琴图、饼图、散点图、Pairplot 和 Jointplot 继续从不同角度看同一批数据。读图时要抓住三个问题变量是否偏态类别之间是否分离变量之间是否存在可学习的边界。图 6 年龄与生存时间 KDE年龄近似钟形分布生存时间明显右偏。图 7 箱线图分期和生存时间的阶梯关系非常清楚性别与年龄差异不大。图 8 小提琴图不同治疗方式的生存时间分布大量重叠提示单变量很难直接决定结局。图 9 饼图Stage IV 与化疗样本占比较高是必须向读者交代的数据背景。图 10 散点图年龄、生存时间、性别、饮酒和体力活动之间没有简单线性边界。图 11 Pairplot把多个变量放在同一张图中看能快速发现分期与生存时间的分层。图 12 年龄与生存时间联合分布高密度区域集中在 55-75 岁、短生存时间附近。图 13 饮酒与生存时间联合分布二值变量常形成两条竖带读图时不要误解成连续趋势。第 3 部分为什么一定要用 Pipeline表格数据里有两类变量数值变量和类别变量。数值变量可以直接参与计算但尺度不同例如年龄是几十二值风险因素是 0/1生存时间是 1-59。类别变量如国家、治疗方式、分期不能直接喂给大多数 sklearn 模型需要编码。如果手工先标准化、再 One-Hot、再切训练测试集很容易把测试集信息泄漏进训练过程。Pipeline 的意义是把“预处理”和“训练”锁在同一条流程里让交叉验证时每一折都只用训练折拟合预处理器。X df.drop(columns[Survival_Status]) y df[Survival_Status] categorical_cols X.select_dtypes(include[object, bool]).columns numerical_cols X.select_dtypes(include[int64, float64]).columns numerical_transformer StandardScaler() categorical_transformer OneHotEncoder(handle_unknownignore) preprocessor ColumnTransformer( transformers[ (num, numerical_transformer, numerical_cols), (cat, categorical_transformer, categorical_cols), ] ) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )用一句费曼式解释Pipeline 就像医院的标准化检查流程。每位病人先按同一规则量体温、验血、填表然后医生再判断。如果检查流程每次都不一样诊断结果就没法比较。第 4 部分12 种模型横向比较单个模型表现好不代表它真的适合数据。更稳妥的做法是把多个模型放到同一预处理流程、同一训练测试划分、同一评价指标下比较。这样比较出来的差异才有意义。models [ (Random Forest, RandomForestClassifier(random_state42)), (Gradient Boosting, GradientBoostingClassifier(random_state42)), (SVM, SVC(random_state42)), (KNN, KNeighborsClassifier()), (Decision Tree, DecisionTreeClassifier(random_state42)), (Naive Bayes, GaussianNB()), (Logistic Regression, LogisticRegression(random_state42)), (XGBoost, XGBClassifier(random_state42)), (LightGBM, LGBMClassifier(random_state42)), (CatBoost, CatBoostClassifier(random_state42, verbose0)), (LDA, LinearDiscriminantAnalysis()), (QDA, QuadraticDiscriminantAnalysis()), ] results [] names [] for name, model in models: pipeline Pipeline(steps[(preprocessor, preprocessor), (model, model)]) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) accuracy accuracy_score(y_test, y_pred) results.append(accuracy) names.append(name) print(f{name}: {accuracy:.4f})模型测试集 Accuracy5 折 CV AccuracyRandom Forest0.87300.8715Gradient Boosting0.87300.8715SVM0.87300.8716KNN0.86000.8582Decision Tree0.75600.7529Naive Bayes0.87300.8716Logistic Regression0.87300.8716XGBoost0.87060.8700LightGBM0.87300.8715CatBoost0.87290.8715LDA0.87300.8716QDA0.41490.4325这个结果最值得讲的地方恰恰不是“最高准确率 0.8730”而是它和多数类基线非常接近。因为 0 类占 87.156%一个什么都不学、永远预测 0 的模型Accuracy 也能达到 0.87156。这就是医学二分类任务最容易踩的坑如果阳性或死亡等少数类才是我们真正关心的类别Accuracy 可能会把模型表现讲得过于乐观。下一步应该补充classification_report、混淆矩阵、ROC-AUC、PR-AUC并考虑类别权重或阈值移动。第 5 部分GridSearchCV 做超参数调优超参数不是模型从数据里学出来的参数而是我们训练前给模型设定的控制旋钮。随机森林里树的数量、最大深度、节点分裂最小样本数都会影响偏差和方差。param_grid { model__n_estimators: [100, 200, 300], model__max_depth: [None, 10, 20, 30], model__min_samples_split: [2, 5, 10], } rf_pipeline Pipeline( steps[ (preprocessor, preprocessor), (model, RandomForestClassifier(random_state42)), ] ) grid_search GridSearchCV(rf_pipeline, param_grid, cv5, scoringaccuracy) grid_search.fit(X_train, y_train) print(fBest parameters: {grid_search.best_params_}) print(fBest cross-validation accuracy: {grid_search.best_score_:.4f}) best_model grid_search.best_estimator_ cv_scores cross_val_score(best_model, X, y, cv5, scoringaccuracy) print(fCross-validation accuracy: {np.mean(cv_scores):.4f})本次随机森林网格搜索得到的最优参数为max_depthNone、min_samples_split5、n_estimators100。最佳交叉验证 Accuracy 为 0.8712最终 5 折交叉验证平均 Accuracy 为 0.8716。这个结果告诉我们调参不是魔法。若数据中的可分信号有限或者评价指标被类别不均衡主导调参只能小幅改变结果不能凭空制造信息。第 6 部分特征重要性调好随机森林后可以提取特征重要性。它的含义不是“因果影响”而是“这个特征在树模型分裂中被使用、并降低不纯度的贡献”。所以它适合做解释线索不适合直接当医学结论。if hasattr(best_model.named_steps[model], feature_importances_): importances best_model.named_steps[model].feature_importances_ feature_names preprocessor.get_feature_names_out() feature_importance_df pd.DataFrame( {Feature: feature_names, Importance: importances} ).sort_values(byImportance, ascendingFalse) print(feature_importance_df.head(15))排名特征重要性1num__Age0.1452162num__Survival_Time_Months0.1303563num__Weight_Loss0.0255954num__Abdominal_Discomfort0.0240025num__Smoking_History0.0234766num__Alcohol_Consumption0.0227997num__Back_Pain0.0227188num__Obesity0.0211729num__Development_of_Type2_Diabetes0.02031910cat__Country_United States0.01983311num__Family_History0.01888312cat__Treatment_Type_Chemotherapy0.01852613cat__Economic_Status_Middle0.01851014cat__Access_to_Healthcare_Medium0.01770915num__Jaundice0.017406可以看到Age 和 Survival_Time_Months 排在最前面。注意若目标是预测生存状态而 Survival_Time_Months 是结局之后才知道的信息那么真实部署时要谨慎预测时拿不到的变量不能放进模型否则会形成“事后信息泄漏”。这是医学建模里比模型选择更重要的问题。第 7 部分保存模型训练完成后把整个 Pipeline 保存下来而不是只保存裸模型。原因很简单预测新样本时必须复用同一套标准化和 One-Hot 规则。import joblib joblib.dump(best_model, best_model.pkl)这样保存出来的best_model.pkl包含预处理器和随机森林模型新数据进来后可以直接调用predict。最后这篇实战真正要学会什么如果只记住一件事请记住机器学习项目的核心不是“哪个模型最强”而是“数据、目标、评价指标、验证方式是否自洽”。本案例里Pipeline 和交叉验证让流程变得规范多模型比较让我们知道不同算法的表现边界而类别不均衡提醒我们Accuracy 不能独自承担医学预测的评价责任。下一步可以继续升级使用StratifiedKFold保持每折类别比例给模型加入class_weight用 PR-AUC 评估少数类画混淆矩阵检查漏判再用 SHAP 分析单个样本为什么被模型判成某类。这样才是从“代码能跑”走向“模型可信”的路径。
12种机器学习模型+交叉验证+Pipeline+网格搜索调参:胰腺癌生存状态预测
12 种机器学习模型 交叉验证 Pipeline 网格搜索调参用 Python 做胰腺癌生存状态预测这篇文章用一份 50000 行的胰腺癌预测数据集从数据理解、EDA 可视化、预处理 Pipeline、多模型横向比较、GridSearchCV 超参数调优、交叉验证到特征重要性完整走一遍医学表格数据的机器学习流程。本文不是医学诊断建议而是一次以第一性原理和费曼学习法来拆解机器学习建模的实战教程。本期摘要第一性原理预测不是“套模型”而是把病人的结构化信息 X 映射到生存状态 y。每一步都要服务于这个映射清洗字段、统一尺度、编码类别、控制数据泄漏、用交叉验证估计泛化能力。数据集50000 条样本24 个字段目标变量是Survival_Status。其中 0 类有 43578 条1 类有 6422 条类别明显不均衡。核心流程先 EDA 看数据脾气再用ColumnTransformer Pipeline把数值标准化、类别 One-Hot 和模型训练绑成一条可复现流水线。模型比较随机森林、梯度提升、SVM、KNN、决策树、朴素贝叶斯、逻辑回归、XGBoost、LightGBM、CatBoost、LDA、QDA 共 12 种模型横向比较。关键提醒很多模型 Accuracy 都在 0.873 左右看似不错但多数类基线本身就是 43578 / 50000 0.87156。医学预测里只看 Accuracy 很危险必须继续看 Recall、Precision、F1、ROC-AUC、PR-AUC 和混淆矩阵。引子把模型讲给没学过机器学习的人听费曼学习法要求我们把复杂概念讲到像日常语言一样直观。这里可以这样理解每个病人是一张表格化病历卡里面有年龄、性别、分期、症状、治疗方式、生活方式和医疗可及性等信息。模型要做的事是看过很多历史病历卡之后学习“哪些组合更可能对应某个生存状态”。第一性原理再往下拆机器学习不神秘它只做三件事。第一把真实世界变成数字第二在数字之间找稳定规律第三用没见过的新样本检验这个规律是否仍然成立。本文的 Pipeline、交叉验证、调参本质上都是为了让这三件事更可靠。第 0 部分环境与数据读取先导入依赖。这里既有传统机器学习模型也有 XGBoost、LightGBM、CatBoost 这类梯度提升框架。医学表格数据通常不是图像也不是长文本结构化表格模型仍然非常有竞争力。import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import warnings warnings.filterwarnings(ignore) from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.naive_bayes import GaussianNB from sklearn.linear_model import LogisticRegression from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis from xgboost import XGBClassifier from lightgbm import LGBMClassifier from catboost import CatBoostClassifier读取 CSV 后第一步不要急着训练模型而是先看形状、字段类型、缺失值和类别取值。你可以把这一步理解成“接诊问诊”先确认病历卡有没有漏项、字段是不是写错、每个变量到底表达什么。df pd.read_csv(pancreatic_cancer_prediction_sample.csv) print(df.shape) print(df.info()) print(df.describe()) print(df.isnull().sum()) categorical_cols df.select_dtypes(include[object]).columns for col in categorical_cols: print(f{col}: {df[col].unique()})本数据集共有50000 行、24 列。目标列Survival_Status是二分类标签0 类 43578 条1 类 6422 条。这个比例会直接影响后面对 Accuracy 的解读。第 1 部分先看数据不要先套模型年龄是医学预测里最常见、也最容易被模型重视的变量。先看年龄分布是为了确认数据是否集中在合理区间是否存在明显异常值。plt.figure(figsize(8, 5)) sns.histplot(df[Age], bins30, kdeTrue, colorblue) plt.title(Age Distribution of Patients) plt.xlabel(Age) plt.ylabel(Frequency) plt.show()图 1 年龄分布样本年龄以 60-70 岁附近最集中符合胰腺癌高发年龄段的常识背景。从图上看年龄主要集中在 60-70 岁附近。用费曼式说法如果一个班级里大多数学生都在同一个年龄段模型就很容易把“年龄段”当作重要线索。后面的特征重要性也印证了这一点。接着看诊断分期与生存时间。第一性原理上分期越晚肿瘤扩散越严重治疗窗口越窄生存时间通常越短。图像的价值就是让这个医学常识在数据里显形。plt.figure(figsize(8, 5)) sns.boxplot(xStage_at_Diagnosis, ySurvival_Time_Months, datadf, palettecoolwarm) plt.title(Survival Time Across Stages of Diagnosis) plt.xlabel(Stage at Diagnosis) plt.ylabel(Survival Time (Months)) plt.show()图 2 不同诊断分期下的生存时间Stage I 的生存时间明显更长Stage IV 更短。治疗方式分布也要看。不是因为柱状图高级而是因为样本数量会影响模型对某类治疗方式的学习。如果某个治疗方式样本很少模型对它的判断就更容易不稳定。sns.countplot(xTreatment_Type, datadf, paletteSet2) plt.title(Distribution of Treatment Types) plt.xlabel(Treatment Type) plt.ylabel(Count) plt.xticks(rotation45) plt.show()图 3 治疗方式分布化疗样本最多其次为放疗和手术。第 2 部分相关性、分布与异常值热力图不是为了好看而是为了回答一个问题变量之间有没有强相关如果两个变量几乎表达同一件事它们会让模型重复计分也会影响解释。plt.figure(figsize(10, 6)) sns.heatmap( df.select_dtypes(include[int64, float64]).corr(), annotTrue, cmapcoolwarm, linewidths0.5, ) plt.title(Correlation Heatmap of Numerical Features) plt.show()图 4 数值特征相关性热力图多数风险因素之间相关性较弱Age 与 Survival_Time_Months 是后续模型最敏感的连续变量。从相关性图看多数变量之间相关性较弱。这意味着模型需要从多变量组合里找信号而不是靠某两个变量的简单线性关系吃遍全场。下面的九宫格计数图把主要离散变量的样本比例放到一起看。第一性原理上分类模型学习的是“特征取值和标签之间的条件分布”。如果某些取值本来就占多数模型可能只是学会了样本比例而不是真正学会疾病风险。fig, axes plt.subplots(3, 3, figsize(18, 12)) sns.countplot(xGender, datadf, palettepastel, axaxes[0, 0]) sns.countplot(xSmoking_History, datadf, palettecoolwarm, axaxes[0, 1]) sns.countplot(xDiabetes, datadf, paletteSet2, axaxes[0, 2]) sns.countplot(xChronic_Pancreatitis, datadf, palettemagma, axaxes[1, 0]) sns.countplot(xStage_at_Diagnosis, datadf, paletteviridis, axaxes[1, 1]) sns.countplot(xTreatment_Type, datadf, paletteBlues, axaxes[1, 2]) sns.countplot(xSurvival_Status, datadf, palettehusl, axaxes[2, 0]) sns.countplot(xAlcohol_Consumption, datadf, paletteSet1, axaxes[2, 1]) sns.countplot(xPhysical_Activity_Level, datadf, palettecrest, axaxes[2, 2]) plt.tight_layout() plt.show()图 5 九个离散变量计数图先看类别比例才能判断模型学到的是信号还是样本占比。连续变量的 KDE、箱线图、小提琴图、饼图、散点图、Pairplot 和 Jointplot 继续从不同角度看同一批数据。读图时要抓住三个问题变量是否偏态类别之间是否分离变量之间是否存在可学习的边界。图 6 年龄与生存时间 KDE年龄近似钟形分布生存时间明显右偏。图 7 箱线图分期和生存时间的阶梯关系非常清楚性别与年龄差异不大。图 8 小提琴图不同治疗方式的生存时间分布大量重叠提示单变量很难直接决定结局。图 9 饼图Stage IV 与化疗样本占比较高是必须向读者交代的数据背景。图 10 散点图年龄、生存时间、性别、饮酒和体力活动之间没有简单线性边界。图 11 Pairplot把多个变量放在同一张图中看能快速发现分期与生存时间的分层。图 12 年龄与生存时间联合分布高密度区域集中在 55-75 岁、短生存时间附近。图 13 饮酒与生存时间联合分布二值变量常形成两条竖带读图时不要误解成连续趋势。第 3 部分为什么一定要用 Pipeline表格数据里有两类变量数值变量和类别变量。数值变量可以直接参与计算但尺度不同例如年龄是几十二值风险因素是 0/1生存时间是 1-59。类别变量如国家、治疗方式、分期不能直接喂给大多数 sklearn 模型需要编码。如果手工先标准化、再 One-Hot、再切训练测试集很容易把测试集信息泄漏进训练过程。Pipeline 的意义是把“预处理”和“训练”锁在同一条流程里让交叉验证时每一折都只用训练折拟合预处理器。X df.drop(columns[Survival_Status]) y df[Survival_Status] categorical_cols X.select_dtypes(include[object, bool]).columns numerical_cols X.select_dtypes(include[int64, float64]).columns numerical_transformer StandardScaler() categorical_transformer OneHotEncoder(handle_unknownignore) preprocessor ColumnTransformer( transformers[ (num, numerical_transformer, numerical_cols), (cat, categorical_transformer, categorical_cols), ] ) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )用一句费曼式解释Pipeline 就像医院的标准化检查流程。每位病人先按同一规则量体温、验血、填表然后医生再判断。如果检查流程每次都不一样诊断结果就没法比较。第 4 部分12 种模型横向比较单个模型表现好不代表它真的适合数据。更稳妥的做法是把多个模型放到同一预处理流程、同一训练测试划分、同一评价指标下比较。这样比较出来的差异才有意义。models [ (Random Forest, RandomForestClassifier(random_state42)), (Gradient Boosting, GradientBoostingClassifier(random_state42)), (SVM, SVC(random_state42)), (KNN, KNeighborsClassifier()), (Decision Tree, DecisionTreeClassifier(random_state42)), (Naive Bayes, GaussianNB()), (Logistic Regression, LogisticRegression(random_state42)), (XGBoost, XGBClassifier(random_state42)), (LightGBM, LGBMClassifier(random_state42)), (CatBoost, CatBoostClassifier(random_state42, verbose0)), (LDA, LinearDiscriminantAnalysis()), (QDA, QuadraticDiscriminantAnalysis()), ] results [] names [] for name, model in models: pipeline Pipeline(steps[(preprocessor, preprocessor), (model, model)]) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) accuracy accuracy_score(y_test, y_pred) results.append(accuracy) names.append(name) print(f{name}: {accuracy:.4f})模型测试集 Accuracy5 折 CV AccuracyRandom Forest0.87300.8715Gradient Boosting0.87300.8715SVM0.87300.8716KNN0.86000.8582Decision Tree0.75600.7529Naive Bayes0.87300.8716Logistic Regression0.87300.8716XGBoost0.87060.8700LightGBM0.87300.8715CatBoost0.87290.8715LDA0.87300.8716QDA0.41490.4325这个结果最值得讲的地方恰恰不是“最高准确率 0.8730”而是它和多数类基线非常接近。因为 0 类占 87.156%一个什么都不学、永远预测 0 的模型Accuracy 也能达到 0.87156。这就是医学二分类任务最容易踩的坑如果阳性或死亡等少数类才是我们真正关心的类别Accuracy 可能会把模型表现讲得过于乐观。下一步应该补充classification_report、混淆矩阵、ROC-AUC、PR-AUC并考虑类别权重或阈值移动。第 5 部分GridSearchCV 做超参数调优超参数不是模型从数据里学出来的参数而是我们训练前给模型设定的控制旋钮。随机森林里树的数量、最大深度、节点分裂最小样本数都会影响偏差和方差。param_grid { model__n_estimators: [100, 200, 300], model__max_depth: [None, 10, 20, 30], model__min_samples_split: [2, 5, 10], } rf_pipeline Pipeline( steps[ (preprocessor, preprocessor), (model, RandomForestClassifier(random_state42)), ] ) grid_search GridSearchCV(rf_pipeline, param_grid, cv5, scoringaccuracy) grid_search.fit(X_train, y_train) print(fBest parameters: {grid_search.best_params_}) print(fBest cross-validation accuracy: {grid_search.best_score_:.4f}) best_model grid_search.best_estimator_ cv_scores cross_val_score(best_model, X, y, cv5, scoringaccuracy) print(fCross-validation accuracy: {np.mean(cv_scores):.4f})本次随机森林网格搜索得到的最优参数为max_depthNone、min_samples_split5、n_estimators100。最佳交叉验证 Accuracy 为 0.8712最终 5 折交叉验证平均 Accuracy 为 0.8716。这个结果告诉我们调参不是魔法。若数据中的可分信号有限或者评价指标被类别不均衡主导调参只能小幅改变结果不能凭空制造信息。第 6 部分特征重要性调好随机森林后可以提取特征重要性。它的含义不是“因果影响”而是“这个特征在树模型分裂中被使用、并降低不纯度的贡献”。所以它适合做解释线索不适合直接当医学结论。if hasattr(best_model.named_steps[model], feature_importances_): importances best_model.named_steps[model].feature_importances_ feature_names preprocessor.get_feature_names_out() feature_importance_df pd.DataFrame( {Feature: feature_names, Importance: importances} ).sort_values(byImportance, ascendingFalse) print(feature_importance_df.head(15))排名特征重要性1num__Age0.1452162num__Survival_Time_Months0.1303563num__Weight_Loss0.0255954num__Abdominal_Discomfort0.0240025num__Smoking_History0.0234766num__Alcohol_Consumption0.0227997num__Back_Pain0.0227188num__Obesity0.0211729num__Development_of_Type2_Diabetes0.02031910cat__Country_United States0.01983311num__Family_History0.01888312cat__Treatment_Type_Chemotherapy0.01852613cat__Economic_Status_Middle0.01851014cat__Access_to_Healthcare_Medium0.01770915num__Jaundice0.017406可以看到Age 和 Survival_Time_Months 排在最前面。注意若目标是预测生存状态而 Survival_Time_Months 是结局之后才知道的信息那么真实部署时要谨慎预测时拿不到的变量不能放进模型否则会形成“事后信息泄漏”。这是医学建模里比模型选择更重要的问题。第 7 部分保存模型训练完成后把整个 Pipeline 保存下来而不是只保存裸模型。原因很简单预测新样本时必须复用同一套标准化和 One-Hot 规则。import joblib joblib.dump(best_model, best_model.pkl)这样保存出来的best_model.pkl包含预处理器和随机森林模型新数据进来后可以直接调用predict。最后这篇实战真正要学会什么如果只记住一件事请记住机器学习项目的核心不是“哪个模型最强”而是“数据、目标、评价指标、验证方式是否自洽”。本案例里Pipeline 和交叉验证让流程变得规范多模型比较让我们知道不同算法的表现边界而类别不均衡提醒我们Accuracy 不能独自承担医学预测的评价责任。下一步可以继续升级使用StratifiedKFold保持每折类别比例给模型加入class_weight用 PR-AUC 评估少数类画混淆矩阵检查漏判再用 SHAP 分析单个样本为什么被模型判成某类。这样才是从“代码能跑”走向“模型可信”的路径。