简介:面向计算机专业学生及数据分析学习者的糖尿病预测实战项目,基于Python编程语言实现线性回归与聚类分析,完整演示了数据加载、缺失值处理、标准化、训练集测试集划分、模型建立与评估等关键流程,代码均经严格调试可本地运行,并附有解释理论背景与代码逻辑的详细文档,能够帮助读者快速掌握数据挖掘在医疗场景中的实际应用。资源包共五个文件,内含两个CSV格式的糖尿病特征及标签数据文件、一个主程序Python脚本以及两份说明文档(Markdown与Word各一份),整体大小约为四百零六KB,文件类型覆盖数据、代码和文档,构成一套可复用的学习模板。该项目源自真实学习实践,经导师指导与助教审定并以九十八分通过评审,目前已有六十三人学习下载,内容难度适中,既能支撑课程大作业或毕业设计,也可作为数据分析入门者的实战练手资料。
1. 这个项目在讲什么、能解决什么问题
拿到一份“使用Python进行线性回归与聚类分析以预测糖尿病的源代码及文档说明”,你大概率会先做三件事:确认数据长什么样、确认模型能不能跑通、确认文档值不值得看。这类项目在机器学习教学里非常常见,依托的通常是sklearn内置的糖尿病数据集load_diabetes():442 个样本、10 个数值特征,目标是连续型的糖尿病进展评分。它的教学价值在于,监督学习部分能用线性回归把“数值预测”这件事讲透,无监督学习部分又能用聚类分析把“样本分群”补进同一个流程里。对新手,这是一条从“跑通一个 demo”到“会组织一个代码仓库”的完整链路;对熟手,真正的信息量在于参数设置、文档规范和数据划分的细节。这篇文章就按照这个标题展开,讲清楚每一份代码和每一段文档应该怎么组织。
2. 拿到代码先别跑:环境、数据和项目结构
很多人拿到这类项目的第一反应是直接pip install然后运行主脚本,结果在数据加载或依赖上浪费时间。正确的做法是先花十分钟理清项目结构和环境,再去看代码逻辑。
2.1.1 一个标准项目的目录长什么样
常见的做法是,把源代码、数据、文档和实验记录分开存放。这个标题既然明确把“源代码及文档说明”并列,说明作者希望交付物本身是可读的。一个典型结构是:
diabetes_project/ ├── README.md ├── requirements.txt ├── LICENSE ├── data/ │ └── diabetes.csv ├── docs/ │ ├── API.md │ └── MODEL_REPORT.md ├── notbooks/ │ └── 01_exploratory_analysis.ipynb ├── scripts/ │ ├── load_data.py │ ├── train_linear.py │ └── cluster_analysis.py └── src/ └── features.pyREADME.md是入口,必须说清楚这个项目做什么、怎么跑通;scripts/存放可执行脚本,而不是把全部代码堆在一个.py里;docs/里的文档说明模型选择和数据含义。对于 600~900 行规模的教学项目,这样的结构已经足够。不要一上来就建src、tests、config全套,小项目撑不起那么大开销。
2.1.2 Python 环境的锁定方式
环境配置最忌“假装做了”。直接用pip freeze > requirements.txt会把所有不相关的包打进列表,正确做法是:
conda create -n diabetes python=3.11 -y conda activate diabetes pip install scikit-learn pandas numpy matplotlib seaborn jupyter pip freeze | grep -E "^(scikit-learn|pandas|numpy|matplotlib|seaborn)" > requirements.txt注意最后一行只导出了核心包和精确版本,这样别人拿到requirements.txt就能复现环境。conda和pip混用的问题存在,但在这个规模的项目里,只要环境名对口,不会出大问题。至于 Python 版本,建议 3.10 或 3.11,因为sklearn在 3.12 上虽然能跑,但部分numpy老代码会报错,没必要给自己加戏。
2.1.3 数据加载与基线观察
项目标题里写的是“源代码”,第一份代码应该就是数据加载与探索。下面是一段可以放进scripts/load_data.py的脚本:
import pandas as pd from sklearn.datasets import load_diabetes diabetes = load_diabetes() df = pd.DataFrame(diabetes.data, columns=diabetes.feature_names) df["target"] = diabetes.target print(df.head()) print(df.info()) print(df.describe().T)load_diabetes()返回的是Bunch对象,data是特征矩阵,target是连续型标签。打印head()只能做形态确认,describe().T的意义在于看量纲。原始数据集里所有特征都做了均值中心化和方差缩放,所以每一列都接近“均值为 0、标准差为 1”,直接建模是可行的,但你自己替换数据时不能默认有这些预处理。
提示:如果
df.describe().T的输出里出现max值与min值相差过大的列,说明这个特征没有标准化,后续回归模型需要手动做StandardScaler。
这里有个段很容易被忽略:load_diabetes返回的数据已经打过乱序,所以直接用train_test_split时不需要再手动 shuffle。但不代表所有项目都这样,复制代码到其他数据时,务必在train_test_split里显式设置shuffle=True。
3. 线性回归:用最小二乘与梯度下降逼近糖尿病进展指标
线性回归是这类预测项目的核心。很多人直接调用LinearRegression拟合,根本不看数据形态和模型假设。这里需要先把原理说明白,再落到参数和评估上,这一章是全文最厚的一层。
3.1.1 为什么线性回归适合这份数据
糖尿病数据集的特征数只有 10,样本量 442,特征之间没有明显的非线性关系先验。对于表格型连续值预测,线性模型是性价比最高的起点。如果一上来就上 XGBoost 或神经网络,第一,你的文档很难解释特征贡献;第二,在这么少的数据下,复杂模型更容易过拟合。线性回归的核心假设是:
$$y = w_1 x_1 + w_2 x_2 + \cdots + w_{10} x_{10} + b$$
训练的实质是求出使残差平方和最小的系数向量 $w$。sklearn的LinearRegression用的是最小二乘法(lstsq),它通过求解正规方程直接得到闭式解,不涉及梯度下降。但在文档里,你还是需要说明为什么在小数据集上选LinearRegression而不是SGDRegressor——前者没有学习率这一超参数,不会因为步长设置不当而发散。
3.1.2 特征处理的三个坑
load_diabetes的 10 个特征虽然已经是数值型,但拿来做回归前有三个反复出现的坑,代码实现时要一起处理:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = diabetes.data y = diabetes.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, shuffle=True ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)第一个坑是fit_transform只能用在训练集上,测试集必须复用训练集拟合好的scaler做transform。如果对测试集单独fit_transform,相当于模型在训练时就已经知道了测试分布的均值和方差,评估结果会虚高。第二个坑是random_state要固定,否则每次运行切分不同,文档里记录的指标无法复现。第三个坑是scaler要在train_test_split之后做,如果在切分前对整个数据集做标准化,存在信息泄漏。
提示:你看到很多人直接对
X做标准化再切分,这在教学代码里很常见,但严格来说必须分开展示。文档里特别提一句“标准化在划分之后进行”,是显功底的。
3.1.3 拟合与评估:一套可以直接抄的代码
下面这段代码可以放进scripts/train_linear.py,它同时输出回归指标和残差图,适合写进文档:
import numpy as np from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score model = LinearRegression() model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"RMSE: {rmse:.2f}") print(f"MAE: {mae:.2f}") print(f"R²: {r2:.3f}") for name, coef in zip(diabetes.feature_names, model.coef_): print(f"{name:>8}: {coef:.2f}")这段代码的model.coef_对应每个特征对进展评分的边际贡献,系数绝对值越大,该特征对预测结果的影响越显著。打印系数不是为了好看,而是方便在文档里做解释。R²的意义是模型解释了 target 方差的多少比例,这个数据集上线性回归的R²通常在 0.4 到 0.5 之间,不属于“效果好”的范围,但这本身就是教学项目的意义。
3.1.4 加惩罚项:Ridge 才是这里的常客
实际项目中,线性回归通常不是终点。Ridge在损失函数里加入 L2 惩罚项,来压缩系数的大小,从而降低过拟合。用代码对比一下不同alpha的效果:
from sklearn.linear_model import Ridge alphas = [0.01, 0.1, 1.0, 10.0] for alpha in alphas: ridge = Ridge(alpha=alpha, max_iter=10000) ridge.fit(X_train_scaled, y_train) ridge_pred = ridge.predict(X_test_scaled) ridge_r2 = r2_score(y_test, ridge_pred) print(f"alpha={alpha:<6} R²={ridge_r2:.3f}")alpha越大,惩罚越强,系数越向 0 收缩。max_iter=10000是给求解器的一个迭代上限,默认值在部分版本里是None,意味着不限制,但手动设置上限能避免个别数据下出现收敛警告。不要只比较训练集上的R²,一定要在测试集上比。如果训练集分数高而测试集分数低,说明模型过拟合,这时候才需要增大alpha。
3.1.5 主模型与扩展的对比结论
把上面的结果汇总成一张表,你会得到类似下面的输出:
| 模型 | R² | RMSE | MAE | 备注 |
|---|---|---|---|---|
| LinearRegression | 0.452 | 59.30 | 47.75 | 普通最小二乘 |
| Ridge(alpha=0.1) | 0.455 | 59.12 | 47.23 | 略微改善 |
| Ridge(alpha=1.0) | 0.454 | 59.15 | 47.29 | 系数收缩均匀 |
| Ridge(alpha=10.0) | 0.410 | 61.52 | 49.05 | 惩罚过度 |
在这份数据上,alpha=0.1的 Ridge 比普通线性回归略好一点点,但差距不大。文档里千万不要写“Ridge 显著优于线性回归”,这种结论基本不成立。真实的项目中,这个差距用交叉验证才能确认是否显著,单次切分的结果只是参考。
波士顿房价线性回归项目常和这个数据集并列出现,区别在于波士顿房价特征之间存在多重共线性,Ridge 的提升更明显。糖尿病数据集的特征已经做过正交化,所以惩罚项的作用更多是稳定系数,而不是提升精度。
4. 聚类分析:无监督视角下的患者分群与特征叠加
标题把“聚类分析”和“线性回归”并列,最容易让人困惑的问题是:预测任务里为什么要做聚类?答案有两个维度。其一,聚类能帮我们发现数据中是否存在天然的病况亚型,比如轻度、中度和高风险人群,这种分群本身就有医学解释价值。其二,聚类的结果可以作为新的特征(cluster_id)叠加到回归模型里,看是否提升预测精度。这一章聚焦后者,因为它既体现了聚类的用法,又能和前面的回归模型串起来。
4.1.1 KMeans 在 sklearn 中的关键参数
聚类分析在 Python 里最常用的是KMeans,在参数设置上有几个值得展开的细节。下面是标准的肘部法则代码,用来确定聚类数:
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score inertia = [] sil_scores = [] for k in range(2, 11): km = KMeans(n_clusters=k, n_init=10, random_state=42) km.fit(X_train_scaled) inertia.append(km.inertia_) labels = km.predict(X_train_scaled) sil = silhouette_score(X_train_scaled, labels) sil_scores.append(sil) print(f"k={k:<2} inertia={km.inertia_:<8.0f} silhouette={sil:.3f}")关键词是n_init。旧版本KMeans默认只跑一次初始化,容易落进局部最优;新版本n_init默认值已经改为auto,但老代码里写n_init=1的并不少见。n_init=10的意思是跑 10 次不同的随机中心点,取惯性最小的那一次结果。random_state在这里不能省,否则每次运行聚类标签都会变,后续“聚类 + 回归”的结果就不可复现。
inertia表示样本到最近中心点的总距离平方和,它随k增大单调递减。肘部法则就是在inertia曲线的折点处选k。silhouette_score是轮廓系数,范围是[-1, 1],越接近 1 表示分群越清晰。
4.1.2 SPPS 聚类与层次聚类的对照思路
如果你用过 SPSS,会发现 SPSS 聚类分析默认输出的是层次聚类(系统聚类)的树状图。Python 里用scipy.cluster.hierarchy也可以做同样的操作,它不需要预设k:
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster import matplotlib.pyplot as plt Z = linkage(X_train_scaled, method="ward") plt.figure(figsize=(10, 5)) dendrogram(Z, truncate_mode="level", p=3) plt.title("Hierarchical Clustering Dendrogram") plt.savefig("docs/dendrogram.png", dpi=150)ward方法要求样本间距离用欧氏距离,所以在传入之前必须确认数据已经标准化。树状图的价值在于,你可以直观看到数据到底可以合并成几类,比直接跑 KMeans 更有解释力。truncate_mode="level", p=3用来压缩显示,否则 442 个样本的叶子节点会糊成一片。
4.1.3 把聚类标签变成回归特征
选出k=3之后,把每个样本的cluster_id作为一个离散特征,和其他 10 个特征拼接,再训练 Ridge,观察效果是否有变化:
import numpy as np km = KMeans(n_clusters=3, n_init=10, random_state=42) km.fit(X_train_scaled) train_cluster_labels = km.predict(X_train_scaled) test_cluster_labels = km.predict(X_test_scaled) X_train_cluster = np.column_stack((X_train_scaled, train_cluster_labels)) X_test_cluster = np.column_stack((X_test_scaled, test_cluster_labels)) from sklearn.linear_model import Ridge model_cluster = Ridge(alpha=0.1) model_cluster.fit(X_train_cluster, y_train) cluster_pred = model_cluster.predict(X_test_cluster) print(f"With cluster feature R²: {r2_score(y_test, cluster_pred):.3f}")注意,聚类模型是在X_train_scaled上拟合的,predict必须分别作用到训练集和测试集。这里很常见的错误是,先对全量数据聚类,再切分训练测试,或者把km.fit和km.predict混着用。聚类本身是无监督的,它不需要y_test,但标签泄漏的坑仍然存在——如果km是在全量数据上拟合的,那么测试集的特征分布已经被模型“看到”了。
在糖尿病数据上,加入聚类标签后的R²通常不会有显著提升。这是因为原始 10 个特征已经包含了大部分有效信息,聚类的结果本质上是这些特征的非线性组合,线性模型无法充分利用。但这不代表聚类没有意义,它给你的文档提供了“人群细分”的分析素材。如果你写文档时把聚类这部分定位成“辅助分析”,而不是“提升精度的手段”,逻辑上就圆满了。
4.1.4 聚类可视化与业务解释
最后的可视化可以这样写:
from sklearn.decomposition import PCA pca = PCA(n_components=2, random_state=42) X_train_pca = pca.fit_transform(X_train_scaled) plt.figure(figsize=(8, 6)) for c in range(km.n_clusters): plt.scatter( X_train_pca[km.labels_ == c, 0], X_train_pca[km.labels_ == c, 1], label=f"Cluster {c}" ) plt.xlabel("PC1") plt.ylabel("PC2") plt.legend() plt.savefig("docs/kmeans_pca.png", dpi=150)用 PCA 降维到二维再进行散点图展示,是文档里最常见的做法。需要说明的是,PCA 之前的数据必须和聚类时用同一份标准化数据,不能用原始值,否则图的分布会误导读者。
5. 文档说明与版本管理:让源码可复现、可交付
标题里“文档说明”四个字,在一份完整的源码交付中占的比重比大多数人想的高。代码本身只是工程的一半,另一半是别的研究者或一周后的你,能按照文档把环境装起来、把步骤复现出来、把结论读懂。
5.1.1 README 中必须出现的五个板块
一个贴合本次项目的 README 不需要长,但目录要完整:
- 项目简介:两到三句话说明用途,不要超过五行
- 环境依赖:Python 版本、核心包及其版本号
- 快速开始:复制粘贴就能跑的命令,从安装依赖到运行脚本,逐步列出
- 结果复现:说明每次运行前需要固定的
random_state和切分比例 - 目录结构:用代码块列出项目结构,并逐一说明用途
模板如下:
## 快速开始 ```bash conda create -n diabetes python=3.11 -y conda activate diabetes pip install -r requirements.txt python scripts/load_data.py python scripts/train_linear.py python scripts/cluster_analysis.py所有脚本输出指标均基于random_state=42,重新运行前请勿修改该参数。
`conda create -n diabetes` 指定环境名,`python=3.11` 固定解释器大版本,`requirements.txt` 里的版本号格式为 `scikit-learn==1.5.2`,不用 `>=`,否则无法保证可复现性。 #### 5.1.2 文档记录模型的每项参数 写文档时最容易遗漏的是“选参理由”。不必长篇大论,用表格列出来最清晰: | 参数 | 值 | 说明 | |------|-----|------| | test_size | 0.2 | 测试集占比,样本量 442 时留出 88 个 | | random_state | 42 | 固定数据切分顺序,保证结果可复现 | | n_init | 10 | KMeans 随机初始化次数,防止局部最优 | | alpha | 0.1 | Ridge 惩罚强度,基于交叉验证选取 | | max_iter | 10000 | 求解器最大迭代次数 | #### 5.1.3 requirements 的精确锁版本 写 `requirements.txt` 时,核心原则是精简和加版本号。下载量最高的那些包比如 `pip` 和 `setuptools` 不要放进去。以下是最小可用清单: ```text numpy==1.26.4 pandas==2.2.2 scikit-learn==1.5.2 matplotlib==3.9.2 seaborn==0.13.2 jupyter==1.0.0如果你发现pandas和numpy的某个版本组合在你的 Python 3.12 环境下安装失败,降级到python=3.11重装即可。这类关于python安装教程和python环境配置的问题,在项目文档里提一句“建议使用 Python 3.11”比解释底层原因更实用。
5.1.4 用实验记录表防止“跑完就忘”
最后分享一个实用的技巧:在项目根目录建docs/experiments.md,每次改动模型参数或数据划分,先录入表格再跑,不要跑完再补。这里的记录格式可以固定为:日期、模型、关键参数、R²、RMSE、备注。这样一周后回头看,你不会面对一堆输出日志而想不起哪一行对应哪个版本。
本文还有配套的精品资源,点击获取