☰
Python+scikit-learn实现癌症预测模型:随机森林二分类实战
2026/10/21 8:48:44 网站建设 项目流程

简介:基于Python实现的癌症预测机器学习项目,适合机器学习初学者、医疗数据分析人员及人工智能相关课程设计使用,演示了从数据准备、特征工程到模型评估的完整流程。压缩包共18个文件,约100KB,包含6个源代码脚本、3个数据表格、2个说明文档以及工程配置文件,结构清晰,便于二次开发。目前已有993人学习,是同类入门项目中较受关注的一份资料。项目内容覆盖数据清洗、缺失值处理、独热编码、特征选择与标准化等关键步骤,并给出了逻辑回归、随机森林等分类模型的训练与评估代码;采用交叉验证与网格搜索调参,可输出准确率、召回率、F1分数等评价指标,帮助理解模型优化方向。通过复现整个项目,读者可掌握一套可迁移的医疗数据分类建模方法,为后续真实场景应用打下基础。

1. 从一份肿瘤数据开始:这个项目在预测什么

这份 python实现预测癌症模型 的资源,核心是一个用 pandas 和 scikit-learn 完成的二分类机器学习项目。输入是一份带诊断标签的肿瘤检测表格数据,记录着细胞核半径、纹理、面积等特征,模型要学的是区分样本属于良性还是恶性,最终输出“新样本有多大概率是恶性”的概率值。它不依赖 GPU,也不牵扯深度学习框架,几百行 Python 就能跑通。适合刚学完机器学习基础、想找完整模板跑通流程的新手,也适合手里有表格数据、想快速落地分类模型的业务开发。资源把数据清洗、标准化、训练、交叉验证、评估、预测拆成独立模块,跑完一遍,对“分类模型怎么被制造出来”会有直观认识。

2. 技术栈与目录结构:机器学习项目的骨架

2.1 技术选型:为什么选 scikit-learn 而不是深度学习

癌症预测这个场景在机器学习落地里有一个典型特征:数据是表格型的,特征列几十个,样本量从几百到几千。面对这种数据,scikit-learn 里的逻辑回归、随机森林、梯度提升树往往比深度学习更实用。原因很直白,表格数据没有图像、文本那样的空间或时序结构,树模型天然能处理数值型和类别型混合的特征,还能自动捕捉特征之间的非线性关系;而神经网络在样本量不足时容易把训练集的噪声也学进去,调参一遍下来要花的时间,够随机森林网格搜索跑好几轮了。

选型时我一般会把逻辑回归当成第一棒,跑个基线看数据本身有没有区分度,再上随机森林这类集成模型。这个项目默认用的是随机森林配合网格搜索,属于表格分类场景里最稳妥的组合之一。树模型本身不要求特征缩放,但项目里还是加了标准化,目的很简单:等你想换成逻辑回归或 SVM 时,不需要回头再改数据处理代码,属于给后续留后路的做法。

2.2 目录结构:每个文件管什么事

cancer-predict/ ├── data/ │ ├── tumor_data.csv # 原始肿瘤诊断数据,只读 │ └── processed/ │ └── train_test.parquet # 清洗和切分后的训练测试数据 ├── src/ │ ├── preprocess.py # 数据清洗、标签编码、标准化 │ ├── train.py # 模型训练 + 网格搜索交叉验证 │ ├── evaluate.py # 在测试集上输出分类报告和指标 │ └── predict.py # 加载模型,对新样本输出概率 ├── models/ │ └── best_model.joblib # 训练产物,joblib 持久化 ├── notebooks/ │ └── eda.ipynb # 探索性数据分析 ├── requirements.txt └── README.md

这个目录结构有三个设计要点。第一,数据目录分原始区和处理区,tumor_data.csv 始终是只读的,清洗逻辑全部收敛在 preprocess.py 里,你不小心把数据改坏时随时可以重跑恢复。第二,训练和预测拆成两个脚本,train.py 负责训练保存模型,predict.py 只负责加载模型和计算概率,线上使用时不需要重新导入训练代码,依赖面更小。第三,notebooks 单独放探索分析,避免把手动分析过程写进正式训练脚本,保证数据管道的可重复性。

四个脚本的交互关系是以文件为界的:preprocess.py 读入 data/tumor_data.csv,输出 parquet 到 data/processed/;train.py 读 parquet,训练后把模型写到 models/best_model.joblib;evaluate.py 读模型和测试集生成指标报告;predict.py 加载模型,对命令行传入的一条新样本输出概率。每个脚本可以单独运行,改数据管道时不会牵动训练逻辑,这是小型机器学习项目里比较清爽的组织方式。

2.3 数据流水线:从 CSV 到概率值的完整链路

一条原始记录变成预测概率,在项目里走五步。第一步,pandas 读入 CSV,先不急着建模,用 df.info() 和 df.isna().sum() 看数据类型和缺失值分布,确认目标列、特征列和无关列。第二步,把诊断列从字符串映射成 0/1,同时丢弃 id 列,因为 id 和诊断结果没有逻辑关系,留着只会让模型去拟合一个无意义的编号。第三步,把数值特征全部过一遍 StandardScaler,标准化公式是(x - mean) / std,让所有特征落在同一量纲上。第四步,按 8:2 切分训练集和测试集,切分时加stratify=y保证两个集合里良恶性比例一致。第五步,训练随机森林,用五折交叉验证搜索参数,把最优模型落盘。

为什么 stratify 在这个项目里几乎是必须的?肿瘤数据的正负样本通常不平衡,恶性样本可能只占 30% 左右,如果随机切分,运气差的时候测试集里恶性比例会偏离原始分布,评估出的准确率就失真了。stratify 的作用就是让切分后的类别比例与原始数据集保持一致,这是表格分类里容易忽略但很关键的一步。特征标准化同样重要,肿瘤特征里面积均值的量纲可能到几千,而纹理标准差可能只有个位数,不标准化的话,基于距离和梯度的模型会不自觉地把权重压在大数值特征上。随机森林本身不受影响,但流水线里提前做好标准化,后面换模型就不用再动数据处理代码。

2.4 依赖管理:几个关键的第三方库

requirements.txt 里核心依赖是 pandas、scikit-learn、matplotlib、joblib、pyarrow,深度学习相关的包一个都没有。pandas 处理表格读写,scikit-learn 提供模型、交叉验证和评估指标,matplotlib 用来画 ROC 曲线和混淆矩阵热力图,joblib 负责模型持久化,pyarrow 是 pandas 写 parquet 格式所需的引擎。

版本选择上我一般建议直接装当前稳定版,但有一个坑要提前说:scikit-learn 的模型持久化存在版本兼容问题,用 1.2 版本训练并保存的 joblib 文件,在 1.4 版本里加载时可能报错,或者更隐蔽地给出不一致的预测结果。如果这个项目只是你自己训练自己用,问题不大;但如果你打算把训练好的模型交给别人复用,最好在 requirements.txt 里锁住 scikit-learn 和 joblib 的主版本号,比如scikit-learn==1.3.2这种写法,避免两端版本错位。这类问题在第五章会再展开一次。

3. 核心模块拆解:从数据清洗到随机森林训练的关键代码

3.1 数据加载与标签编码

先看预处理模块的核心逻辑。这一步放在 preprocess.py 里,控制台执行python src/preprocess.py就会得到一份干净的 parquet 文件:

import pandas as pd def load_and_clean(raw_path: str) -> pd.DataFrame: df = pd.read_csv(raw_path) print(df.shape, df.isna().sum().sum()) # 目标列从字符串映射成 0/1,良性 B 记为 0,恶性 M 记为 1 df['diagnosis'] = df['diagnosis'].map({'B': 0, 'M': 1}) # id 与诊断结果无关,丢掉的目的是防止模型学习无意义编号 if 'id' in df.columns: df = df.drop(columns=['id']) # 用返回布尔值的 mask 过滤掉映射失败的脏数据 df = df[df['diagnosis'].isin([0, 1])] return df if __name__ == '__main__': df = load_and_clean('data/tumor_data.csv') df.to_parquet('data/processed/clean.parquet')

这段代码里值得注意的是 map 操作。map 是 pandas 在 Series 上的逐元素映射,比 for 循环快得多,但如果字典里没有对应键,结果会变成 NaN,所以我在 map 之后又做了一次过滤,用isin([0, 1])把非法标签行直接剔除。这样即使原始数据里出现拼写错误的标签,模型训练前也会被拦下来,而不是带着脏数据一路跑进训练流程。

放弃 id 列你可能会舍不得,总觉得“多一列特征总比少一列好”。但在表格分类里,编号类特征没有任何泛化意义,模型的表达能力会被浪费在一个随机变量上,偶尔还会因为某条样本 id 数值极端而扰动树的切分。这类特征的删除越早越好,清洗阶段就要下手。

3.2 特征标准化与训练测试集切分

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler def prepare_features(df: pd.DataFrame): X = df.drop(columns=['diagnosis']) y = df['diagnosis'] 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 = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 测试集只 transform,不 fit return X_train, X_test, y_train, y_test, scaler

这段代码是整个项目里最容易写错的地方。fit_transform和transform的关系是:scaler 在训练集上 fit,算出每个特征的均值和标准差,然后对训练集做变换;测试集只用同一套参数做 transform。如果有人顺手对 X_test 也调了 fit_transform,就会发生数据泄漏,测试集的信息提前进入预处理参数,评估出来的准确率虚高,模型上线后马上打回原形。

提示:训练集 fit_transform、测试集 transform 是一条铁律,不只是 StandardScaler,PCA、归一化等所有无监督预处理都遵守同一个原则。

random_state=42 的作用是固定随机种子,让每次切分结果一致。做实验时如果切分不固定,模型指标忽高忽低,你很难判断改动到底是让模型变好了还是只是运气变好了。固定种子之后,前后两次训练的唯一变量就是代码本身,对比才有意义。

3.3 网格搜索交叉验证训练随机森林

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV def train_model(X_train, y_train): param_grid = { 'n_estimators': [50, 100, 200], 'max_depth': [3, 5, None], 'min_samples_leaf': [1, 2, 4], } grid = GridSearchCV( RandomForestClassifier(random_state=42), param_grid, cv=5, scoring='roc_auc', n_jobs=-1, ) grid.fit(X_train, y_train) print('best_auc:', round(grid.best_score_, 4)) print('best_params:', grid.best_params_) return grid.best_estimator_

这里的 param_grid 定义了 3×3×3 = 27 种参数组合,每种组合跑 5 折交叉验证,总共 135 次训练。单棵随机森林在千行数据上的训练耗时通常不到一秒,n_jobs=-1 表示用满所有 CPU 核心,整体跑完也就一两分钟。第一次跑完你会看到控制台输出 best_auc 和 best_params,这两个值对应交叉验证里的平均得分和最优参数组合。

scoring 用 roc_auc 而不是 accuracy,原因是面对不平衡数据,准确率有很强的迷惑性:就算模型把所有样本都预测成良性这个多数类,准确率也能有七成以上,但模型实际上完全没有区分能力。AUC 衡量的是模型把正样本排在负样本前面的能力,对类别不平衡更鲁棒,这是医疗预测项目里最基本的一条评估原则。max_depth 和 min_samples_leaf 是控制树复杂度、抑制过拟合的两个关键参数。max_depth 限制树生长层数,None 表示不限制;min_samples_leaf 要求叶子节点至少包含指定数量样本,设太大容易欠拟合,太小又容易把噪声学进去。网格搜索结束后,best_estimator_ 就是 27 组里交叉验证得分最高的那个模型,后续评估和预测都使用它。

3.4 测试集评估与新样本预测

from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix import joblib def evaluate(model, X_test, y_test): y_pred = model.predict(X_test) y_prob = model.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred)) print('roc_auc:', round(roc_auc_score(y_test, y_prob), 4)) print(confusion_matrix(y_test, y_pred)) joblib.dump(model, 'models/best_model.joblib') def predict_single(model_path, sample_array): model = joblib.load(model_path) proba = model.predict_proba(sample_array)[:, 1] return proba[0]

predict_proba 返回的是二维数组,第一列是良性概率,第二列是恶性概率,代码里取[:, 1]拿第二列。项目对外输出的核心其实是这个概率值,而不是 predict 的 0/1 硬标签,因为临床场景里决策更关注置信度:概率 0.7 和 0.3 的处理策略完全不同,只输出一个类别会丢掉大量信息。这类医疗辅助决策项目里,保留概率输出而不是只打印分类结果,是刻意设计的选择。

evaluate 函数里同时打印了分类报告和混淆矩阵。混淆矩阵能告诉你具体错在哪里:假阴性是真正的恶性样本被预测成了良性,在癌症筛查场景里这是最不可接受的错误;假阳性带来的代价是让患者做多余的复查,虽然浪费资源但至少不会漏诊。看懂这四个数字,比盯着一个准确率数字有用得多。

4. 训练与评估实操:参数调优与效果验证

4.1 环境准备与运行命令

拿到资源第一步是创建虚拟环境,这部分我习惯用 conda:

conda create -n cancer-predict python=3.10 -y conda activate cancer-predict pip install -r requirements.txt

requirements.txt 里没有 GPU 相关依赖,装起来几分钟。数据文件放到 data/tumor_data.csv 之后,按三条命令就能跑完整流程:

python src/preprocess.py # 清洗数据,生成干净的 parquet python src/train.py # 网格搜索训练,保存 best_model.joblib python src/evaluate.py # 加载模型,输出分类报告和指标

如果你是第一次跑,建议先用小参数集验证流程通不通,比如把 param_grid 临时改成{'n_estimators': [50]},快速跑一遍确认没有报错,再放开完整参数。train.py 跑起来会有一段时间的静默期,如果发现控制台长时间没有输出,优先检查是不是 n_jobs=-1 把 CPU 占满导致其他进程响应变慢,可以临时把 n_jobs 调成 2 再对比速度。

4.2 评估指标怎么读

训练结束后的 classification_report 里,重点看三个指标。精确率表示预测为恶性的样本里真正恶性的比例,召回率表示真实恶性样本里被正确找出来的比例。在癌症筛查场景,召回率优先级高于精确率:宁可多安排一些不必要的复查,也不要漏掉一个真恶性样本。如果报告里召回率偏低,一个常用的做法是把分类阈值从 0.5 下调到 0.3,让模型更容易把样本判为恶性,代价是误报增多,但漏诊率会降下来。

AUC 是另一个维度,衡量模型把正样本排在负样本前面的能力。0.95 以上的 AUC 说明模型的排序能力很强,但单独看 AUC 并不够,还是要结合混淆矩阵确认错在哪。有个经验:AUC 很高但召回率很差的情况完全可能同时出现,因为漏掉的那几个恶性样本如果排名在 0.4 到 0.5 之间,并没有破坏整体的排序结构,AUC 依然很高。所以评估报告、AUC、混淆矩阵三者必须一起看,缺一不可。

4.3 网格搜索参数说明与调整方向

整理一份常用的参数调整表,照着这个方向改基本不会跑偏:

参数默认值作用调整方向
n_estimators100树的数量,越大越稳定样本量大时加到 300~500,效果边际递减
max_depthNone树的深度,控制复杂度数据量少时限制在 3~5 防过拟合
min_samples_leaf1叶子节点最小样本数过拟合时调大到 4~8
max_features'sqrt'每次分裂考虑的特征数特征多时试试 'log2' 或固定值
class_weightNone类别权重,缓解不平衡不平衡明显时设 'balanced'

class_weight 是容易被忽略但很有用的参数。恶性样本占比明显偏低时,设成 'balanced' 会让模型在训练时给少数类更高的惩罚权重,相当于强制模型更关注恶性样本,召回率往往会有明显提升,代价是精确率可能下降。业务的取舍要看场景:筛查场景更看重召回率,诊断确认场景更看重精确率。这个参数在随机森林里直接传,换成逻辑回归也一样生效。

4.4 训练过程监控与异常判断

我习惯在训练时把关键位置的状态打印出来,而不是让脚本安安静静跑完。标准化之后打印 scaler.mean_,能直观看到每个特征的均值是否合理;网格搜索每跑完一组就打印当前参数和得分,方便中途判断哪个方向更值得继续搜。如果交叉验证得分比测试集得分高出 0.1 以上,大概率存在数据泄漏,重点检查预处理是否在切分前就做了全量数据的 fit。

另外要留意标准差为 0 的特征。如果一个特征的方差是 0,标准化后整列全是 0,模型在这列上得不到任何信息,还白白浪费一次分裂尝试。出现这种情况时,应该回到 preprocess.py 把该列过滤掉。这种问题在原始数据里很常见,但经常被埋没在“反正模型也能跑”的沉默里,打印中间状态是性价比最高的排除方式。

5. 避坑与常见问题排查:五个真实踩坑记录

跑这类医疗预测项目,最容易出问题的不是模型选型,而是数据流里那些你以为理所当然的地方。这一章把我在这个项目上踩过的五个坑,按“现象、原因、解决”整理出来,每一个都在真实运行里出现过,也都有对应的排查路径。

5.1 模型把所有样本预测成良性

第一次跑完,分类报告显示精确率 0.98,但恶性样本的召回率是 0。原因是训练集里恶性样本占比只有两成左右,随机森林学到的策略是“全部预测良性”成本最低,准确率依然有八成以上,但模型完全没区分能力。

解决方式有三步同时做:scoring 换成 roc_auc,避免网格搜索按准确率挑模型;模型参数里加class_weight='balanced',给少数类更高的权重;确认 train_test_split 用了 stratify=y。三条都做完,测试集上的召回率通常会有明显改善。这类项目里,指标选错比模型选错更致命。

5.2 测试集评估指标虚高

把 X_test 也拿去 fit_transform 之后,测试集上的 AUC 高得离谱,0.98 看起来惊艳。原因就是数据泄漏:测试集的统计信息提前进入了标准化参数,模型在评估时已经“见过”测试集的分布。

解决方式:scaler 只在训练集上 fit,测试集一律 transform。这个坑对逻辑回归和 SVM 的伤害尤其明显,随机森林因为是树模型所以影响相对小,但流水线既然已经做了标准化,就要从头到尾遵守“训练集 fit、测试集 transform”这条原则。如果你发现测试集指标比交叉验证高出一截,先怀疑这个位置。

5.3 joblib 加载时模型不兼容

在一台机器上训练保存的模型,换到另一台机器加载时报错,或者不报错但预测结果对不上。原因是 scikit-learn 版本不一致,不同版本之间模型的内部表示可能变化,joblib 只负责把对象序列化和反序列化,不负责跨版本兼容。

解决方式:requirements.txt 里锁住 scikit-learn 和 joblib 的版本,两端用完全相同的虚拟环境。加载模型时打印model.__class__和sklearn.__version__做一次自检,确认对象类型和版本符合预期,再进入预测流程。模型交付给别人时,把 requirements.txt 一起给是最基本的礼貌。

5.4 网格搜索一跑就是半小时

参数组合多、交叉验证折数多、树也深,三者叠一起计算量就爆了。比如 27 组参数配 10 折交叉验证就是 270 次训练,每次训练里还有几百棵树,跑起来容易让人误以为脚本卡死了。

解决方式:先在小数据子集上用少参数组合验证流程,比如df.sample(500)配合单参数跑一遍;n_jobs=-1 把并行度拉满;实验阶段用 cv=3,最终确定参数后再用 cv=5 做正式训练。网格搜索的价值在于缩小参数范围,不在一次跑完所有可能性。

5.5 不同机器上结果不可复现

同一份代码,在两台机器上训练出的模型指标略有差异。原因是浮点运算顺序、线程并行时的随机数使用都会引入不确定性,树模型尤其明显。

解决方式:给模型和切分都固定 random_state,并设置 numpy 的随机种子。如果项目对可复现性要求很高,做一次规范化:在 train.py 入口处统一random.seed(42)、np.random.seed(42),同时给 RandomForestClassifier 传入 random_state。这样至少保证同一环境下实验可复现,换机器时差异也能控制在很小的范围内。

6. 二次开发思路:把模型换成你的数据集和业务场景

6.1 替换数据集时最少要改的几个地方

换成自己的 CSV 数据时,核心改动集中在三处。第一处是 preprocess.py 的列名适配:把 diagnosis 换成任务目标列,把 id 删除逻辑换成自己的无关列。第二处是标签映射:如果是多分类,map 字典要扩展成多个类别;如果是回归任务,整个分类评估部分都要换成回归指标,比如 MSE 和 R2。第三处是 param_grid 搜索范围:数据量变大时 n_estimators 可以往上调,特征数量变多时 max_features 可以试试 'log2'。

数据替换之后先做一次快速验证,用df.describe()对比新旧数据集的特征范围,确认没有极端异常值进入训练。最怕的是新数据集里混入了字符串列,StandardScaler 会直接报错,所以替换后先跑 preprocess.py,不要跳过清洗直接训练。

6.2 切换模型与概率校准

from sklearn.ensemble import GradientBoostingClassifier from sklearn.calibration import CalibratedClassifierCV base = GradientBoostingClassifier( max_depth=3, learning_rate=0.1, n_estimators=120, random_state=42, ) calibrated = CalibratedClassifierCV(base, cv=3, method='isotonic') calibrated.fit(X_train, y_train)

把随机森林换成梯度提升树,只改一次模型实例化,流水线其他代码完全不用动。替换之后我建议多做一步:用 CalibratedClassifierCV 对概率做校准,让模型输出的概率更接近真实频率。随机森林的概率输出有时会偏激进,比如把 0.6 的样本直接给到 0.9,概率校准会在交叉验证里学习一个修正映射,让输出概率和实际比例对齐。医疗场景里概率值本身就会被当作决策依据,这一步值得加。

6.3 落地部署的最小方案

脚本能跑通只是第一步,真正要交付给别人用,我一般会再补三件事。第一件,把 predict.py 改成一个接收 JSON、返回概率的 HTTP 接口,用你熟悉的轻量 Web 框架包一层,路由里完成标准化和模型预测。第二件,把 scaler 和模型一起打包,预测请求进来时先标准化再喂给模型,避免接口侧重复实现数据变换逻辑。第三件,写一个接口自测脚本,模拟一条新样本请求,验证输入输出格式和概率范围是否符合预期。

从那以后我每次做表格分类项目,都强制走一遍“先看类别分布、再切分、再评估、再看混淆矩阵”的固定顺序,这套流程跑熟了,踩坑的概率低很多。希望帮到你。

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

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

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

立即咨询